hfs 3.0.7 → 3.1.0-alpha2

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 (317) hide show
  1. package/README.md +1 -1
  2. package/admin/assets/index-CDiYkO8j.css +1 -0
  3. package/admin/assets/index-Dm5BN1iZ.js +1 -0
  4. package/admin/assets/index-P5i8LyWl.js +822 -0
  5. package/admin/assets/{sha512-gDcjPgJS.js → sha512-DG-ElrDe.js} +1 -1
  6. package/admin/flags/freakflags.css +284 -0
  7. package/admin/flags/sprite.png +0 -0
  8. package/admin/index.html +3 -2
  9. package/frontend/assets/index-legacy-CG_og8xL.js +1 -0
  10. package/frontend/assets/index-legacy-DDLKWGcm.js +9 -0
  11. package/frontend/assets/{sha512-legacy-BBwwadDl.js → sha512-legacy-D1zEZzDe.js} +1 -1
  12. package/frontend/fontello.css +3 -1
  13. package/frontend/fontello.woff2 +0 -0
  14. package/frontend/index.html +1 -1
  15. package/npm-shrinkwrap.json +16409 -0
  16. package/package.json +12 -11
  17. package/plugins/antibrute/plugin.js +14 -3
  18. package/src/AsapStream.js +49 -0
  19. package/src/ThrottledStream.js +1 -1
  20. package/src/adminApis.js +16 -5
  21. package/src/api.accounts.js +1 -6
  22. package/src/api.auth.js +103 -100
  23. package/src/api.get_file_list.js +7 -0
  24. package/src/api.lang.js +1 -0
  25. package/src/api.log.js +5 -3
  26. package/src/api.monitor.js +5 -0
  27. package/src/api.net.js +5 -0
  28. package/src/api.plugins.js +13 -2
  29. package/src/api.vfs.js +41 -28
  30. package/src/apiMiddleware.js +5 -4
  31. package/src/auth.js +39 -6
  32. package/src/commands.js +16 -27
  33. package/src/comments.js +4 -4
  34. package/src/config.js +5 -2
  35. package/src/const.js +2 -16
  36. package/src/cross-const.js +5 -2
  37. package/src/cross.js +21 -15
  38. package/src/expiringCache.js +16 -5
  39. package/src/fileAttr.js +1 -1
  40. package/src/first.js +7 -4
  41. package/src/frontEndApis.js +88 -102
  42. package/src/github.js +32 -21
  43. package/src/ips.js +1 -1
  44. package/src/log.js +5 -3
  45. package/src/middlewares.js +2 -1
  46. package/src/misc.js +2 -20
  47. package/src/perm.js +27 -12
  48. package/src/persistence.js +1 -1
  49. package/src/plugins.js +12 -6
  50. package/src/serveFile.js +3 -3
  51. package/src/serveGuiAndSharedFiles.js +3 -0
  52. package/src/serveGuiFiles.js +2 -1
  53. package/src/srp.js +5 -6
  54. package/src/stat.js +4 -1
  55. package/src/update.js +18 -25
  56. package/src/upload.js +4 -3
  57. package/src/util-files.js +2 -1
  58. package/src/util-http.js +3 -3
  59. package/src/vfs.js +4 -2
  60. package/src/webdav.js +297 -0
  61. package/admin/assets/index-Bj9Dk3JN.js +0 -822
  62. package/admin/assets/index-Byv1_NxP.css +0 -1
  63. package/admin/flags/ad.png +0 -0
  64. package/admin/flags/ae.png +0 -0
  65. package/admin/flags/af.png +0 -0
  66. package/admin/flags/ag.png +0 -0
  67. package/admin/flags/ai.png +0 -0
  68. package/admin/flags/al.png +0 -0
  69. package/admin/flags/am.png +0 -0
  70. package/admin/flags/ao.png +0 -0
  71. package/admin/flags/aq.png +0 -0
  72. package/admin/flags/ar.png +0 -0
  73. package/admin/flags/as.png +0 -0
  74. package/admin/flags/at.png +0 -0
  75. package/admin/flags/au.png +0 -0
  76. package/admin/flags/aw.png +0 -0
  77. package/admin/flags/ax.png +0 -0
  78. package/admin/flags/az.png +0 -0
  79. package/admin/flags/ba.png +0 -0
  80. package/admin/flags/bb.png +0 -0
  81. package/admin/flags/bd.png +0 -0
  82. package/admin/flags/be.png +0 -0
  83. package/admin/flags/bf.png +0 -0
  84. package/admin/flags/bg.png +0 -0
  85. package/admin/flags/bh.png +0 -0
  86. package/admin/flags/bi.png +0 -0
  87. package/admin/flags/bj.png +0 -0
  88. package/admin/flags/bl.png +0 -0
  89. package/admin/flags/bm.png +0 -0
  90. package/admin/flags/bn.png +0 -0
  91. package/admin/flags/bo.png +0 -0
  92. package/admin/flags/bq.png +0 -0
  93. package/admin/flags/br.png +0 -0
  94. package/admin/flags/bs.png +0 -0
  95. package/admin/flags/bt.png +0 -0
  96. package/admin/flags/bv.png +0 -0
  97. package/admin/flags/bw.png +0 -0
  98. package/admin/flags/by.png +0 -0
  99. package/admin/flags/bz.png +0 -0
  100. package/admin/flags/ca.png +0 -0
  101. package/admin/flags/cc.png +0 -0
  102. package/admin/flags/cd.png +0 -0
  103. package/admin/flags/cf.png +0 -0
  104. package/admin/flags/cg.png +0 -0
  105. package/admin/flags/ch.png +0 -0
  106. package/admin/flags/ci.png +0 -0
  107. package/admin/flags/ck.png +0 -0
  108. package/admin/flags/cl.png +0 -0
  109. package/admin/flags/cm.png +0 -0
  110. package/admin/flags/cn.png +0 -0
  111. package/admin/flags/co.png +0 -0
  112. package/admin/flags/cr.png +0 -0
  113. package/admin/flags/cu.png +0 -0
  114. package/admin/flags/cv.png +0 -0
  115. package/admin/flags/cw.png +0 -0
  116. package/admin/flags/cx.png +0 -0
  117. package/admin/flags/cy.png +0 -0
  118. package/admin/flags/cz.png +0 -0
  119. package/admin/flags/de.png +0 -0
  120. package/admin/flags/dj.png +0 -0
  121. package/admin/flags/dk.png +0 -0
  122. package/admin/flags/dm.png +0 -0
  123. package/admin/flags/do.png +0 -0
  124. package/admin/flags/dz.png +0 -0
  125. package/admin/flags/ec.png +0 -0
  126. package/admin/flags/ee.png +0 -0
  127. package/admin/flags/eg.png +0 -0
  128. package/admin/flags/eh.png +0 -0
  129. package/admin/flags/er.png +0 -0
  130. package/admin/flags/es.png +0 -0
  131. package/admin/flags/et.png +0 -0
  132. package/admin/flags/fi.png +0 -0
  133. package/admin/flags/fj.png +0 -0
  134. package/admin/flags/fk.png +0 -0
  135. package/admin/flags/fm.png +0 -0
  136. package/admin/flags/fo.png +0 -0
  137. package/admin/flags/fr.png +0 -0
  138. package/admin/flags/ga.png +0 -0
  139. package/admin/flags/gb-eng.png +0 -0
  140. package/admin/flags/gb-nir.png +0 -0
  141. package/admin/flags/gb-sct.png +0 -0
  142. package/admin/flags/gb-wls.png +0 -0
  143. package/admin/flags/gb.png +0 -0
  144. package/admin/flags/gd.png +0 -0
  145. package/admin/flags/ge.png +0 -0
  146. package/admin/flags/gf.png +0 -0
  147. package/admin/flags/gg.png +0 -0
  148. package/admin/flags/gh.png +0 -0
  149. package/admin/flags/gi.png +0 -0
  150. package/admin/flags/gl.png +0 -0
  151. package/admin/flags/gm.png +0 -0
  152. package/admin/flags/gn.png +0 -0
  153. package/admin/flags/gp.png +0 -0
  154. package/admin/flags/gq.png +0 -0
  155. package/admin/flags/gr.png +0 -0
  156. package/admin/flags/gs.png +0 -0
  157. package/admin/flags/gt.png +0 -0
  158. package/admin/flags/gu.png +0 -0
  159. package/admin/flags/gw.png +0 -0
  160. package/admin/flags/gy.png +0 -0
  161. package/admin/flags/hk.png +0 -0
  162. package/admin/flags/hm.png +0 -0
  163. package/admin/flags/hn.png +0 -0
  164. package/admin/flags/hr.png +0 -0
  165. package/admin/flags/ht.png +0 -0
  166. package/admin/flags/hu.png +0 -0
  167. package/admin/flags/id.png +0 -0
  168. package/admin/flags/ie.png +0 -0
  169. package/admin/flags/il.png +0 -0
  170. package/admin/flags/im.png +0 -0
  171. package/admin/flags/in.png +0 -0
  172. package/admin/flags/io.png +0 -0
  173. package/admin/flags/iq.png +0 -0
  174. package/admin/flags/ir.png +0 -0
  175. package/admin/flags/is.png +0 -0
  176. package/admin/flags/it.png +0 -0
  177. package/admin/flags/je.png +0 -0
  178. package/admin/flags/jm.png +0 -0
  179. package/admin/flags/jo.png +0 -0
  180. package/admin/flags/jp.png +0 -0
  181. package/admin/flags/ke.png +0 -0
  182. package/admin/flags/kg.png +0 -0
  183. package/admin/flags/kh.png +0 -0
  184. package/admin/flags/ki.png +0 -0
  185. package/admin/flags/km.png +0 -0
  186. package/admin/flags/kn.png +0 -0
  187. package/admin/flags/kp.png +0 -0
  188. package/admin/flags/kr.png +0 -0
  189. package/admin/flags/kw.png +0 -0
  190. package/admin/flags/ky.png +0 -0
  191. package/admin/flags/kz.png +0 -0
  192. package/admin/flags/la.png +0 -0
  193. package/admin/flags/lb.png +0 -0
  194. package/admin/flags/lc.png +0 -0
  195. package/admin/flags/li.png +0 -0
  196. package/admin/flags/lk.png +0 -0
  197. package/admin/flags/lr.png +0 -0
  198. package/admin/flags/ls.png +0 -0
  199. package/admin/flags/lt.png +0 -0
  200. package/admin/flags/lu.png +0 -0
  201. package/admin/flags/lv.png +0 -0
  202. package/admin/flags/ly.png +0 -0
  203. package/admin/flags/ma.png +0 -0
  204. package/admin/flags/mc.png +0 -0
  205. package/admin/flags/md.png +0 -0
  206. package/admin/flags/me.png +0 -0
  207. package/admin/flags/mf.png +0 -0
  208. package/admin/flags/mg.png +0 -0
  209. package/admin/flags/mh.png +0 -0
  210. package/admin/flags/mk.png +0 -0
  211. package/admin/flags/ml.png +0 -0
  212. package/admin/flags/mm.png +0 -0
  213. package/admin/flags/mn.png +0 -0
  214. package/admin/flags/mo.png +0 -0
  215. package/admin/flags/mp.png +0 -0
  216. package/admin/flags/mq.png +0 -0
  217. package/admin/flags/mr.png +0 -0
  218. package/admin/flags/ms.png +0 -0
  219. package/admin/flags/mt.png +0 -0
  220. package/admin/flags/mu.png +0 -0
  221. package/admin/flags/mv.png +0 -0
  222. package/admin/flags/mw.png +0 -0
  223. package/admin/flags/mx.png +0 -0
  224. package/admin/flags/my.png +0 -0
  225. package/admin/flags/mz.png +0 -0
  226. package/admin/flags/na.png +0 -0
  227. package/admin/flags/nc.png +0 -0
  228. package/admin/flags/ne.png +0 -0
  229. package/admin/flags/nf.png +0 -0
  230. package/admin/flags/ng.png +0 -0
  231. package/admin/flags/ni.png +0 -0
  232. package/admin/flags/nl.png +0 -0
  233. package/admin/flags/no.png +0 -0
  234. package/admin/flags/np.png +0 -0
  235. package/admin/flags/nr.png +0 -0
  236. package/admin/flags/nu.png +0 -0
  237. package/admin/flags/nz.png +0 -0
  238. package/admin/flags/om.png +0 -0
  239. package/admin/flags/pa.png +0 -0
  240. package/admin/flags/pe.png +0 -0
  241. package/admin/flags/pf.png +0 -0
  242. package/admin/flags/pg.png +0 -0
  243. package/admin/flags/ph.png +0 -0
  244. package/admin/flags/pk.png +0 -0
  245. package/admin/flags/pl.png +0 -0
  246. package/admin/flags/pm.png +0 -0
  247. package/admin/flags/pn.png +0 -0
  248. package/admin/flags/pr.png +0 -0
  249. package/admin/flags/ps.png +0 -0
  250. package/admin/flags/pt.png +0 -0
  251. package/admin/flags/pw.png +0 -0
  252. package/admin/flags/py.png +0 -0
  253. package/admin/flags/qa.png +0 -0
  254. package/admin/flags/re.png +0 -0
  255. package/admin/flags/ro.png +0 -0
  256. package/admin/flags/rs.png +0 -0
  257. package/admin/flags/ru.png +0 -0
  258. package/admin/flags/rw.png +0 -0
  259. package/admin/flags/sa.png +0 -0
  260. package/admin/flags/sb.png +0 -0
  261. package/admin/flags/sc.png +0 -0
  262. package/admin/flags/sd.png +0 -0
  263. package/admin/flags/se.png +0 -0
  264. package/admin/flags/sg.png +0 -0
  265. package/admin/flags/sh.png +0 -0
  266. package/admin/flags/si.png +0 -0
  267. package/admin/flags/sj.png +0 -0
  268. package/admin/flags/sk.png +0 -0
  269. package/admin/flags/sl.png +0 -0
  270. package/admin/flags/sm.png +0 -0
  271. package/admin/flags/sn.png +0 -0
  272. package/admin/flags/so.png +0 -0
  273. package/admin/flags/sr.png +0 -0
  274. package/admin/flags/ss.png +0 -0
  275. package/admin/flags/st.png +0 -0
  276. package/admin/flags/sv.png +0 -0
  277. package/admin/flags/sx.png +0 -0
  278. package/admin/flags/sy.png +0 -0
  279. package/admin/flags/sz.png +0 -0
  280. package/admin/flags/tc.png +0 -0
  281. package/admin/flags/td.png +0 -0
  282. package/admin/flags/tf.png +0 -0
  283. package/admin/flags/tg.png +0 -0
  284. package/admin/flags/th.png +0 -0
  285. package/admin/flags/tj.png +0 -0
  286. package/admin/flags/tk.png +0 -0
  287. package/admin/flags/tl.png +0 -0
  288. package/admin/flags/tm.png +0 -0
  289. package/admin/flags/tn.png +0 -0
  290. package/admin/flags/to.png +0 -0
  291. package/admin/flags/tr.png +0 -0
  292. package/admin/flags/tt.png +0 -0
  293. package/admin/flags/tv.png +0 -0
  294. package/admin/flags/tw.png +0 -0
  295. package/admin/flags/tz.png +0 -0
  296. package/admin/flags/ua.png +0 -0
  297. package/admin/flags/ug.png +0 -0
  298. package/admin/flags/um.png +0 -0
  299. package/admin/flags/us.png +0 -0
  300. package/admin/flags/uy.png +0 -0
  301. package/admin/flags/uz.png +0 -0
  302. package/admin/flags/va.png +0 -0
  303. package/admin/flags/vc.png +0 -0
  304. package/admin/flags/ve.png +0 -0
  305. package/admin/flags/vg.png +0 -0
  306. package/admin/flags/vi.png +0 -0
  307. package/admin/flags/vn.png +0 -0
  308. package/admin/flags/vu.png +0 -0
  309. package/admin/flags/wf.png +0 -0
  310. package/admin/flags/ws.png +0 -0
  311. package/admin/flags/xk.png +0 -0
  312. package/admin/flags/ye.png +0 -0
  313. package/admin/flags/yt.png +0 -0
  314. package/admin/flags/za.png +0 -0
  315. package/admin/flags/zm.png +0 -0
  316. package/admin/flags/zw.png +0 -0
  317. package/frontend/assets/index-legacy-COBT1YmS.js +0 -50
package/src/srp.js CHANGED
@@ -3,19 +3,18 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.srpClientSequence = srpClientSequence;
5
5
  exports.srpClientPart = srpClientPart;
6
- const tssrp6a_1 = require("tssrp6a");
7
- async function srpClientSequence(username, password, apiCall, extra) {
6
+ async function srpClientSequence(srp, username, password, apiCall, extra) {
8
7
  const { pubKey, salt } = await apiCall('loginSrp1', { username });
9
8
  if (!salt)
10
9
  throw Error('salt');
11
- const client = await srpClientPart(username, password, salt, pubKey);
10
+ const client = await srpClientPart(srp, username, password, salt, pubKey);
12
11
  const res = await apiCall('loginSrp2', { pubKey: String(client.A), proof: String(client.M1), ...extra }); // bigint-s must be cast to string to be json-ed
13
12
  await client.step3(BigInt(res.proof)).catch(() => Promise.reject('trust'));
14
13
  return res;
15
14
  }
16
- async function srpClientPart(username, password, salt, pubKey) {
17
- const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
18
- const srpClient = new tssrp6a_1.SRPClientSession(srp6aNimbusRoutines);
15
+ async function srpClientPart(srp, username, password, salt, pubKey) {
16
+ const srp6aNimbusRoutines = new srp.SRPRoutines(new srp.SRPParameters());
17
+ const srpClient = new srp.SRPClientSession(srp6aNimbusRoutines);
19
18
  const res = await srpClient.step1(username, password);
20
19
  return await res.step2(BigInt(salt), BigInt(pubKey));
21
20
  }
package/src/stat.js CHANGED
@@ -4,9 +4,12 @@ exports.getStatWorker = getStatWorker;
4
4
  const node_worker_threads_1 = require("node:worker_threads");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const cross_1 = require("./cross");
7
+ const first_1 = require("./first");
7
8
  // all stat requests for the same worker are serialized, potentially introducing extra latency
8
9
  const pool = new Map();
9
10
  function getStatWorker(key) {
11
+ if (first_1.quitting)
12
+ return () => Promise.reject('quitting');
10
13
  const existing = pool.get(key);
11
14
  if (existing)
12
15
  return existing;
@@ -15,7 +18,7 @@ function getStatWorker(key) {
15
18
  const requests = new Map();
16
19
  worker.on('message', (msg) => {
17
20
  const k = msg.path;
18
- requests.get(k)?.resolve(msg.error ? Promise.reject(new Error(msg.error))
21
+ requests.get(k)?.resolve(msg.error ? Promise.reject(Error(msg.error))
19
22
  : Object.setPrototypeOf(msg.result, node_fs_1.Stats.prototype));
20
23
  requests.delete(k);
21
24
  });
package/src/update.js CHANGED
@@ -19,7 +19,6 @@ const misc_1 = require("./misc");
19
19
  const fs_1 = require("fs");
20
20
  const plugins_1 = require("./plugins");
21
21
  const promises_1 = require("fs/promises");
22
- const open_1 = __importDefault(require("open"));
23
22
  const config_1 = require("./config");
24
23
  const util_os_1 = require("./util-os");
25
24
  const first_1 = require("./first");
@@ -134,14 +133,18 @@ async function update(tagOrUrl = '') {
134
133
  }
135
134
  if (url) {
136
135
  console.log("downloading", url);
137
- const { body } = await (0, misc_1.httpWithBody)(url);
138
- if (!body)
139
- throw "Download failed for " + url;
140
- await (0, promises_1.writeFile)(LOCAL_UPDATE, body);
136
+ try {
137
+ await (0, promises_1.writeFile)(LOCAL_UPDATE, await (0, misc_1.httpStream)(url));
138
+ }
139
+ catch (e) {
140
+ await (0, promises_1.rm)(LOCAL_UPDATE).catch(() => { }); // no leftovers
141
+ throw "Download failed for " + url + (0, misc_1.prefix)(' – ', e?.message);
142
+ }
143
+ console.debug("download finished");
141
144
  }
142
145
  const bin = process.execPath;
143
146
  const binPath = (0, path_1.dirname)(bin);
144
- const binFile = 'hfs' + (const_1.IS_WINDOWS ? '.exe' : ''); // currently running bin could have been renamed
147
+ const binFile = 'hfs' + (const_1.IS_WINDOWS ? '.exe' : ''); // the bin we are currently running could have been renamed
145
148
  let newBinFile = binFile;
146
149
  do {
147
150
  newBinFile = 'new-' + newBinFile;
@@ -170,7 +173,7 @@ async function update(tagOrUrl = '') {
170
173
  catch { }
171
174
  (0, fs_1.renameSync)(bin, oldBin);
172
175
  console.log("launching new version in background", newBinFile);
173
- launch(newBin, ['--updating', binFile, '--cwd .'], { sync: true }); // sync necessary to work on Mac by double-click
176
+ (0, child_process_1.spawnSync)((0, util_os_1.cmdEscape)(newBin), ['--updating', binFile, '--cwd .'], { shell: true, stdio: [0, 1, 2] }); // sync necessary to work on Mac by double-click
174
177
  });
175
178
  console.log("quitting");
176
179
  setTimeout(() => process.exit()); // give time to return (and caller to complete, eg: rest api to reply)
@@ -180,28 +183,18 @@ async function update(tagOrUrl = '') {
180
183
  throw e?.message || String(e);
181
184
  }
182
185
  }
183
- function launch(cmd, pars = [], options) {
184
- return (options?.sync ? child_process_1.spawnSync : child_process_1.spawn)((0, util_os_1.cmdEscape)(cmd), pars, { detached: true, shell: true, stdio: [0, 1, 2], ...options });
185
- }
186
186
  if (argv_1.argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
187
187
  const bin = process.execPath;
188
188
  const dest = (0, path_1.join)((0, path_1.dirname)(bin), argv_1.argv.updating);
189
189
  (0, fs_1.renameSync)(bin, dest);
190
- // have to relaunch with the new name, or otherwise next update will fail with EBUSY on hfs.exe
190
+ // have to relaunch with the new name, or otherwise the next update will fail with EBUSY on hfs.exe
191
191
  console.log(`renamed binary file to "${argv_1.argv.updating}" and now restarting`);
192
- // be sure to test launching both double-clicking and in a terminal
193
- if (const_1.IS_WINDOWS) // this method on Mac works only once, and without console
194
- (0, first_1.onProcessExit)(() => launch(dest, ['--updated', '--cwd .'])); // launch+sync here would cause old process to stay open, locking ports
195
- else {
196
- /* open() is the only consistent way that I could find working on macos preserving console input/output over relaunching,
197
- * but I couldn't find a way to pass parameters, at least on Linux. The workaround I'm using is to write them to a temp file, that's read and deleted at restart.
198
- * For the record, on mac you can: write "./hfs arg1 arg2" to /tmp/tmp.sh with 0o700, and then spawn "open -a Terminal /tmp/tmp.sh"
199
- */
200
- try {
201
- (0, fs_1.writeFileSync)(const_1.ARGS_FILE, JSON.stringify(['--updated', '--cwd', process.cwd().replaceAll(' ', '\\ ')]));
202
- }
203
- catch { }
204
- void (0, open_1.default)(dest);
205
- }
192
+ // if you change anything, be sure to test launching both double-clicking and in a terminal
193
+ if (const_1.IS_WINDOWS) // windows-only; this method on Mac works only once, and without the console
194
+ (0, first_1.onProcessExit)(() => (0, child_process_1.spawn)((0, util_os_1.cmdEscape)(dest), ['--updated', '--cwd .'], { detached: true, shell: true, stdio: [0, 1, 2] })); // launch+sync here would cause the old process to stay open, locking ports
195
+ else if (process.stdin.isTTY && process.stdout.isTTY) // keep interactive terminal users attached to the restarted process
196
+ (0, child_process_1.spawnSync)(dest, ['--updated', '--cwd', process.cwd()], { stdio: [0, 1, 2] });
197
+ else
198
+ (0, child_process_1.spawn)(dest, ['--updated', '--cwd', process.cwd()], { detached: true, stdio: 'ignore' }).unref();
206
199
  process.exit();
207
200
  }
package/src/upload.js CHANGED
@@ -54,7 +54,7 @@ const diskSpaceCache = (0, expiringCache_1.expiringCache)(3_000); // invalidate
54
54
  const uploadingFiles = new Map();
55
55
  // initially sync for formidable; still sync to avoid async races and PUT piping gaps
56
56
  function uploadWriter(base, baseUri, filename, ctx) {
57
- if (!filename || !(0, misc_1.isValidFileName)(filename))
57
+ if (!filename || !(0, misc_1.isValidFileName)(filename) || !filename)
58
58
  return fail(const_1.HTTP_FOOL);
59
59
  if ((0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx))
60
60
  return fail();
@@ -133,10 +133,10 @@ function uploadWriter(base, baseUri, filename, ctx) {
133
133
  fs_1.default.unlinkSync(tempName);
134
134
  const writeStream = (0, misc_1.createStreamLimiter)(contentLength ?? Infinity);
135
135
  const fullSize = stillToWrite + resume;
136
- ctx.state.uploadDestinationPath = tempName;
137
136
  // allow plugins to mess with the write-stream, because the read-stream can be complicated in case of multipart
138
137
  const obj = { ctx, writeStream, fullPath, tempName, resume, fullSize, uri: '' };
139
138
  const resEvent = events_1.default.emit('uploadStart', obj);
139
+ ctx.state.uploadDestinationPath = tempName;
140
140
  if (resEvent?.isDefaultPrevented())
141
141
  return;
142
142
  const fileStream = fs_1.default.createWriteStream(tempName, resume ? { flags: 'r+', start: resume } : undefined);
@@ -282,7 +282,8 @@ function uploadWriter(base, baseUri, filename, ctx) {
282
282
  if (msg)
283
283
  ctx.body = msg;
284
284
  if (status >= 400 // with other codes Chrome will report ERR_CONNECTION_RESET
285
- && !ctx.get('x-hfs-wait')) // you can disable the following behavior
285
+ && !ctx.get('x-hfs-wait') // you can disable the following behavior
286
+ && !ctx.req.complete) // if request body is already complete, forcing a disconnect can interfere with follow-up requests on reused sockets.
286
287
  setTimeout(() => (0, connections_1.disconnect)(ctx), 200); // don't wait, if the upload is still in progress
287
288
  }
288
289
  }
package/src/util-files.js CHANGED
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseFileCache = void 0;
8
8
  exports.statWithTimeout = statWithTimeout;
9
+ exports.getUncHost = getUncHost;
9
10
  exports.isDirectory = isDirectory;
10
11
  exports.readFileWithBusyRetry = readFileWithBusyRetry;
11
12
  exports.watchDir = watchDir;
@@ -163,7 +164,7 @@ async function createSafeWriteStream(path, options) {
163
164
  });
164
165
  }
165
166
  function isValidFileName(name, acceptUnreadable = false) {
166
- return name !== '.' && !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !hasDirTraversal(name)
167
+ return name && name !== '.' && !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !hasDirTraversal(name)
167
168
  && (acceptUnreadable || !/[\u0000-\u001F\u007F]/.test(name));
168
169
  }
169
170
  function exists(path) {
package/src/util-http.js CHANGED
@@ -112,15 +112,15 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
112
112
  delete hostJar[k];
113
113
  }
114
114
  if (!res.statusCode || httpThrow && res.statusCode >= 400)
115
- return reject(new Error(String(res.statusCode), { cause: res }));
115
+ return reject(Error(String(res.statusCode), { cause: res }));
116
116
  let r = res.headers.location;
117
117
  if (r && !noRedirect) {
118
118
  const dest = new URL(r, url); // rewrite in case r is just a path, and thus relative to the current url
119
119
  r = dest.toString();
120
120
  const src = new URL(url);
121
121
  const sameOrigin = src.protocol === dest.protocol && src.host === dest.host;
122
- return redirected.includes(r) ? reject(new Error('endless http redirection'))
123
- : redirected.length > 20 ? reject(new Error('excessive http redirection'))
122
+ return redirected.includes(r) ? reject(Error('endless http redirection'))
123
+ : redirected.length > 20 ? reject(Error('excessive http redirection'))
124
124
  : resolve(httpStream(r, {
125
125
  httpThrow, jar, proxy,
126
126
  ...lodash_1.default.pick(options, ['agent', 'rejectUnauthorized', 'timeout']),
package/src/vfs.js CHANGED
@@ -170,10 +170,12 @@ async function getNodeByName(name, parent, assumeMissingToBeFolder = false) {
170
170
  return ret;
171
171
  }
172
172
  }
173
+ const smartUncFolderDetection = (0, config_1.defineConfig)('smart_unc_folder_detection', true);
173
174
  async function setIsFolder(node) {
174
175
  if (!node.source)
175
176
  return;
176
- const isFolder = /[\\/]$/.test(node.source) || await nodeStats(node).then(x => x?.isDirectory(), () => undefined);
177
+ const isFolder = 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 you can opt-out
178
+ : /[\\/]$/.test(node.source) || await nodeStats(node).then(x => x?.isDirectory(), () => undefined);
177
179
  (0, misc_1.setHidden)(node, { isFolder });
178
180
  return isFolder;
179
181
  }
@@ -328,7 +330,7 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
328
330
  stream.push(null);
329
331
  return null;
330
332
  }
331
- if ((0, comments_1.usingDescriptIon)() && entry.name === comments_1.DESCRIPT_ION)
333
+ if ((0, comments_1.usingDescriptIon)() && (entry.name === comments_1.DESCRIPT_ION || entry.name === comments_1.DESCRIPT_ION_ALT))
332
334
  return;
333
335
  const { path } = entry; // this path is not the original deprecated property: we are overwriting/reusing it
334
336
  const isFolder = entry.isDirectory();
package/src/webdav.js ADDED
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handledWebdav = handledWebdav;
4
+ const vfs_1 = require("./vfs");
5
+ const cross_1 = require("./cross");
6
+ const stream_1 = require("stream");
7
+ const promises_1 = require("fs/promises");
8
+ const misc_1 = require("./misc");
9
+ const path_1 = require("path");
10
+ const frontEndApis_1 = require("./frontEndApis");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const const_1 = require("./const");
13
+ const child_process_1 = require("child_process");
14
+ const auth_1 = require("./auth");
15
+ const config_1 = require("./config");
16
+ const expiringCache_1 = require("./expiringCache");
17
+ const forceWebdavLogin = (0, config_1.defineConfig)(cross_1.CFG.force_webdav_login, true, compileWebdavAgentRegex);
18
+ const webdavInitialAuth = (0, config_1.defineConfig)(cross_1.CFG.webdav_initial_auth, 'WebDAVFS', compileWebdavAgentRegex);
19
+ const webdavPrompted = (0, expiringCache_1.expiringCache)(cross_1.DAY);
20
+ const webdavDetectedAgents = new Set();
21
+ const TOKEN_HEADER = 'lock-token';
22
+ const WEBDAV_METHODS = new Set(['PROPFIND', 'PROPPATCH', 'MKCOL', 'MOVE', 'LOCK', 'UNLOCK']);
23
+ const WEBDAV_HINT_HEADERS = ['depth', 'destination', 'overwrite', 'translate', 'if', TOKEN_HEADER, 'x-expected-entity-length'];
24
+ const KNOWN_UA = /webdav|miniredir|davclnt/i;
25
+ const canOverwrite = new Set();
26
+ const locks = new Map();
27
+ function isLocked(path, ctx) {
28
+ const lock = locks.get(path);
29
+ if (!lock)
30
+ return false;
31
+ const ifHeader = ctx.get('If');
32
+ const tokenHeader = ctx.get(TOKEN_HEADER);
33
+ if (hasToken(ifHeader, lock.token) || hasToken(tokenHeader, lock.token))
34
+ return false;
35
+ ctx.status = cross_1.HTTP_LOCKED;
36
+ return true;
37
+ }
38
+ function hasToken(header, token) {
39
+ if (!header)
40
+ return false;
41
+ return header.includes(`<${token}>`) || header.split(/[,;\s]+/).includes(token);
42
+ }
43
+ async function handledWebdav(ctx) {
44
+ const { path } = ctx;
45
+ const isWebdavAuthRequest = WEBDAV_METHODS.has(ctx.method) || WEBDAV_HINT_HEADERS.some(h => !!ctx.get(h));
46
+ const ua = ctx.get('user-agent');
47
+ if (isWebdavAuthRequest && (0, auth_1.getCurrentUsername)(ctx)) {
48
+ if (ua)
49
+ webdavDetectedAgents.add(ua);
50
+ }
51
+ if (ctx.path.includes('/._') && ua?.startsWith('WebDAVFS')) { // too much spam from Finder for these files that can contain metas
52
+ ctx.state.dontLog = true;
53
+ return ctx.status = cross_1.HTTP_FORBIDDEN;
54
+ }
55
+ if (ctx.method === 'OPTIONS') {
56
+ if (ctx.get('Access-Control-Request-Method'))
57
+ return; // it's a preflight cors request, not webdav
58
+ setWebdavHeaders();
59
+ ctx.body = '';
60
+ return true;
61
+ }
62
+ if (isWebdavAuthRequest && shouldChallengeWebdav())
63
+ return true;
64
+ if (ctx.method === 'PUT') {
65
+ if (isLocked(path, ctx))
66
+ return true;
67
+ // Finder first creates an empty file (a test?) then wants to overwrite it, which requires deletion permission, but the user may not have it, causing a renamed upload. To solve, so we give it special permission for a few seconds.
68
+ const x = ctx.get('x-expected-entity-length'); // field used by Finder's webdav on actual upload, after
69
+ if (!x && !ctx.length) {
70
+ canOverwrite.add(path);
71
+ setTimeout(() => canOverwrite.delete(path), 10_000); // grace period
72
+ }
73
+ else if (canOverwrite.has(path)) {
74
+ canOverwrite.delete(path);
75
+ const node = await (0, vfs_1.urlToNode)(path, ctx);
76
+ if (node?.source)
77
+ await (0, promises_1.rm)(node.source).catch(() => { });
78
+ }
79
+ if (x && ctx.length === undefined) // missing length can make PUT fail
80
+ ctx.req.headers['content-length'] = x;
81
+ if (KNOWN_UA.test(ua) || webdavDetectedAgents.has(ua))
82
+ ctx.query.existing ??= 'overwrite'; // with webdav this is our default
83
+ return; // default handling
84
+ }
85
+ if (ctx.method === 'MKCOL') {
86
+ setWebdavHeaders();
87
+ if (isLocked(path, ctx))
88
+ return true;
89
+ const node = await (0, vfs_1.urlToNode)(path, ctx);
90
+ if (node)
91
+ return ctx.status = cross_1.HTTP_METHOD_NOT_ALLOWED;
92
+ let name = '';
93
+ const parentNode = await (0, vfs_1.urlToNode)(path, ctx, vfs_1.vfs, v => name = v);
94
+ if (!parentNode)
95
+ return ctx.status = cross_1.HTTP_NOT_FOUND;
96
+ if (!(0, misc_1.isValidFileName)(name))
97
+ return ctx.status = cross_1.HTTP_BAD_REQUEST;
98
+ if ((0, vfs_1.statusCodeForMissingPerm)(parentNode, 'can_upload', ctx)) {
99
+ if (ctx.status === cross_1.HTTP_UNAUTHORIZED)
100
+ setWebdavHeaders(true);
101
+ return true;
102
+ }
103
+ try {
104
+ await (0, promises_1.mkdir)((0, path_1.join)(parentNode.source, name));
105
+ return ctx.status = cross_1.HTTP_CREATED;
106
+ }
107
+ catch (e) {
108
+ return ctx.status = cross_1.HTTP_SERVER_ERROR;
109
+ }
110
+ }
111
+ if (ctx.method === 'MOVE') {
112
+ setWebdavHeaders();
113
+ if (isLocked(path, ctx))
114
+ return true;
115
+ const node = await (0, vfs_1.urlToNode)(path, ctx);
116
+ if (!node)
117
+ return;
118
+ let dest = ctx.get('destination');
119
+ const i = dest.indexOf('//');
120
+ if (i >= 0)
121
+ dest = dest.slice(dest.indexOf('/', i + 2));
122
+ if (isLocked(dest, ctx))
123
+ return true;
124
+ if ((0, path_1.dirname)(path) === (0, path_1.dirname)(dest)) // rename. `path` is is encoded, so we test before decoding `dest`
125
+ try {
126
+ await (0, frontEndApis_1.requestedRename)(node, (0, path_1.basename)(decodeURI(dest)), ctx);
127
+ return ctx.status = cross_1.HTTP_CREATED;
128
+ }
129
+ catch (e) {
130
+ return ctx.status = e.status || cross_1.HTTP_SERVER_ERROR;
131
+ }
132
+ const moveRes = await (0, frontEndApis_1.moveFiles)([path], (0, path_1.dirname)(dest), ctx);
133
+ if (moveRes instanceof Error)
134
+ return ctx.status = moveRes.status || cross_1.HTTP_SERVER_ERROR;
135
+ const err = moveRes?.errors?.[0];
136
+ return ctx.status = !err ? cross_1.HTTP_CREATED : typeof err === 'number' ? err : cross_1.HTTP_SERVER_ERROR;
137
+ }
138
+ if (ctx.method === 'DELETE') {
139
+ setWebdavHeaders();
140
+ if (isLocked(path, ctx))
141
+ return true;
142
+ return; // allow default handling in serveGuiAndSharedFiles.ts
143
+ }
144
+ if (ctx.method === 'UNLOCK') {
145
+ setWebdavHeaders();
146
+ const x = ctx.get(TOKEN_HEADER).slice(1, -1);
147
+ const lock = locks.get(path);
148
+ if (x !== lock?.token)
149
+ return ctx.status = cross_1.HTTP_BAD_REQUEST;
150
+ clearTimeout(lock.timeout);
151
+ locks.delete(path);
152
+ ctx.set(TOKEN_HEADER, x);
153
+ if (const_1.IS_MAC)
154
+ (0, vfs_1.urlToNode)(path, ctx).then(x => x?.source && dotClean((0, path_1.dirname)(x.source)));
155
+ return ctx.status = cross_1.HTTP_NO_CONTENT;
156
+ }
157
+ if (ctx.method === 'LOCK') {
158
+ setWebdavHeaders();
159
+ if (locks.has(path))
160
+ return ctx.status = 423;
161
+ const token = 'urn:uuid:' + (0, node_crypto_1.randomUUID)();
162
+ ctx.set(TOKEN_HEADER, token);
163
+ const seconds = 3600;
164
+ const timeout = setTimeout(() => locks.delete(path), seconds * 1000);
165
+ locks.set(path, { token, timeout });
166
+ ctx.body = `<?xml version="1.0" encoding="utf-8"?><prop xmlns="DAV:"><lockdiscovery><activelock>
167
+ <locktype><write/></locktype>
168
+ <lockscope><exclusive/></lockscope>
169
+ <locktoken><href>${token}</href></locktoken>
170
+ <lockroot><href>${path}</href></lockroot>
171
+ <depth>0</depth>
172
+ <timeout>Second-${seconds}</timeout>
173
+ </activelock></lockdiscovery></prop>`;
174
+ return true;
175
+ }
176
+ if (ctx.method === 'PROPFIND') {
177
+ setWebdavHeaders();
178
+ const node = await (0, vfs_1.urlToNode)(path, ctx);
179
+ if (!node)
180
+ return;
181
+ let depth = Number(ctx.get('depth'));
182
+ depth = isNaN(depth) ? Infinity : depth;
183
+ const isList = depth !== 0;
184
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, isList ? 'can_list' : 'can_see', ctx)) {
185
+ if (ctx.status === cross_1.HTTP_UNAUTHORIZED)
186
+ setWebdavHeaders(true);
187
+ return true;
188
+ }
189
+ ctx.type = 'xml';
190
+ ctx.status = 207;
191
+ const pathSlash = (0, cross_1.enforceFinal)('/', path);
192
+ const res = ctx.body = new stream_1.PassThrough({ encoding: 'utf8' });
193
+ res.write(`<?xml version="1.0" encoding="utf-8" ?><multistatus xmlns="DAV:">`);
194
+ await sendEntry(node);
195
+ if (isList) {
196
+ depth = Math.max(0, depth - 1);
197
+ for await (const n of (0, vfs_1.walkNode)(node, { ctx, depth }))
198
+ await sendEntry(n, true);
199
+ }
200
+ res.write(`</multistatus>`);
201
+ res.end();
202
+ return true;
203
+ async function sendEntry(node, append = false) {
204
+ if ((0, vfs_1.nodeIsLink)(node))
205
+ return;
206
+ const name = (0, vfs_1.getNodeName)(node);
207
+ const isDir = await (0, vfs_1.nodeIsFolder)(node);
208
+ const st = await (0, vfs_1.nodeStats)(node);
209
+ res.write(`<response>
210
+ <href>${pathSlash + (append ? (0, cross_1.pathEncode)(name, true) + (isDir ? '/' : '') : '')}</href>
211
+ <propstat>
212
+ <status>HTTP/1.1 200 OK</status>
213
+ <prop>
214
+ ${(0, cross_1.prefix)('<getlastmodified>', st?.mtime?.toGMTString(), '</getlastmodified>')}
215
+ ${(0, cross_1.prefix)('<creationdate>', (st?.birthtime || st?.ctime)?.toISOString().replace(/\..*/, '-00:00'), '</creationdate>')}
216
+ ${isDir ? '<resourcetype><collection/></resourcetype>'
217
+ : `<resourcetype/><getcontentlength>${st?.size}</getcontentlength>`}
218
+ </prop>
219
+ </propstat>
220
+ </response>
221
+ `);
222
+ }
223
+ }
224
+ if (ctx.method === 'PROPPATCH') {
225
+ setWebdavHeaders();
226
+ if (isLocked(path, ctx))
227
+ return true;
228
+ const node = await (0, vfs_1.urlToNode)(path, ctx);
229
+ if (!node)
230
+ return;
231
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_see', ctx)) {
232
+ if (ctx.status === cross_1.HTTP_UNAUTHORIZED)
233
+ setWebdavHeaders(true);
234
+ return true;
235
+ }
236
+ ctx.type = 'xml';
237
+ ctx.status = 207;
238
+ ctx.body = `<?xml version="1.0" encoding="utf-8"?>
239
+ <multistatus xmlns="DAV:">
240
+ <response>
241
+ <href>${path}</href>
242
+ <propstat>
243
+ <status>HTTP/1.1 200 OK</status>
244
+ <prop/>
245
+ </propstat>
246
+ </response>
247
+ </multistatus>`;
248
+ return true;
249
+ }
250
+ function setWebdavHeaders(authenticate = false) {
251
+ ctx.set('DAV', '1,2');
252
+ ctx.set('MS-Author-Via', 'DAV');
253
+ ctx.set('Allow', 'PROPFIND,PROPPATCH,OPTIONS,DELETE,MOVE,LOCK,UNLOCK,MKCOL,PUT');
254
+ if (authenticate)
255
+ ctx.set('WWW-Authenticate', `Basic realm="HFS WebDAV"`); // keep a dedicated realm for WebDAV so Windows credential cache is isolated from other basic-auth flows
256
+ }
257
+ function shouldChallengeWebdav() {
258
+ if ((0, auth_1.getCurrentUsername)(ctx))
259
+ return false;
260
+ if (forceWebdavLogin.compiled()?.test(ua))
261
+ return challengeWebdav();
262
+ if (!webdavInitialAuth.compiled()?.test(ua))
263
+ return false;
264
+ if (ctx.get('authorization'))
265
+ return challengeWebdav();
266
+ const key = `${ctx.ip}|${ctx.host}|${ua || ''}`;
267
+ if (webdavPrompted.has(key))
268
+ return false;
269
+ webdavPrompted.try(key, () => true);
270
+ return challengeWebdav();
271
+ function challengeWebdav() {
272
+ setWebdavHeaders(true);
273
+ ctx.status = cross_1.HTTP_UNAUTHORIZED;
274
+ ctx.body = '';
275
+ return true;
276
+ }
277
+ }
278
+ }
279
+ function compileWebdavAgentRegex(v) {
280
+ return !v ? null : v === true ? /.*/ : new RegExp(v.trim(), 'i');
281
+ }
282
+ // Finder will upload special attributes as files with name ._* that can be merged using system utility "dot_clean"
283
+ const cleaners = {};
284
+ function dotClean(path) {
285
+ (0, cross_1.getOrSet)(cleaners, path, () => setTimeout(() => {
286
+ try {
287
+ (0, child_process_1.exec)('dot_clean .', { cwd: path }, (err, out) => done(err || out));
288
+ }
289
+ catch (e) {
290
+ done(e);
291
+ }
292
+ function done(log) {
293
+ console.debug('dot_clean', path, log);
294
+ delete cleaners[path];
295
+ }
296
+ }, 10_000));
297
+ }