@backblaze-labs/b2-sdk 0.3.0 → 0.4.0

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 (393) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +18 -5
  3. package/dist/_virtual/_b2-sdk-version-json.cjs +1 -1
  4. package/dist/_virtual/_b2-sdk-version-json.js +1 -1
  5. package/dist/auth/reauth-coalescer.cjs +65 -0
  6. package/dist/auth/reauth-coalescer.cjs.map +1 -0
  7. package/dist/auth/reauth-coalescer.d.cts +34 -0
  8. package/dist/auth/reauth-coalescer.d.cts.map +1 -0
  9. package/dist/auth/reauth-coalescer.d.ts +34 -0
  10. package/dist/auth/reauth-coalescer.d.ts.map +1 -0
  11. package/dist/auth/reauth-coalescer.js +65 -0
  12. package/dist/auth/reauth-coalescer.js.map +1 -0
  13. package/dist/backup/raw.cjs +37 -15
  14. package/dist/backup/raw.cjs.map +1 -1
  15. package/dist/backup/raw.d.cts +2 -2
  16. package/dist/backup/raw.d.cts.map +1 -1
  17. package/dist/backup/raw.d.ts +2 -2
  18. package/dist/backup/raw.d.ts.map +1 -1
  19. package/dist/backup/raw.js +37 -15
  20. package/dist/backup/raw.js.map +1 -1
  21. package/dist/bucket-validation.cjs +111 -0
  22. package/dist/bucket-validation.cjs.map +1 -0
  23. package/dist/bucket-validation.d.cts +27 -0
  24. package/dist/bucket-validation.d.cts.map +1 -0
  25. package/dist/bucket-validation.d.ts +27 -0
  26. package/dist/bucket-validation.d.ts.map +1 -0
  27. package/dist/bucket-validation.js +109 -0
  28. package/dist/bucket-validation.js.map +1 -0
  29. package/dist/bucket.cjs +123 -73
  30. package/dist/bucket.cjs.map +1 -1
  31. package/dist/bucket.d.cts +156 -61
  32. package/dist/bucket.d.cts.map +1 -1
  33. package/dist/bucket.d.ts +156 -61
  34. package/dist/bucket.d.ts.map +1 -1
  35. package/dist/bucket.js +123 -73
  36. package/dist/bucket.js.map +1 -1
  37. package/dist/client.cjs +29 -8
  38. package/dist/client.cjs.map +1 -1
  39. package/dist/client.d.cts +30 -9
  40. package/dist/client.d.cts.map +1 -1
  41. package/dist/client.d.ts +30 -9
  42. package/dist/client.d.ts.map +1 -1
  43. package/dist/client.js +29 -8
  44. package/dist/client.js.map +1 -1
  45. package/dist/download/single.cjs +38 -13
  46. package/dist/download/single.cjs.map +1 -1
  47. package/dist/download/single.d.cts +10 -4
  48. package/dist/download/single.d.cts.map +1 -1
  49. package/dist/download/single.d.ts +10 -4
  50. package/dist/download/single.d.ts.map +1 -1
  51. package/dist/download/single.js +38 -13
  52. package/dist/download/single.js.map +1 -1
  53. package/dist/errors/index.cjs +27 -4
  54. package/dist/errors/index.cjs.map +1 -1
  55. package/dist/errors/index.d.cts +15 -0
  56. package/dist/errors/index.d.cts.map +1 -1
  57. package/dist/errors/index.d.ts +15 -0
  58. package/dist/errors/index.d.ts.map +1 -1
  59. package/dist/errors/index.js +27 -5
  60. package/dist/errors/index.js.map +1 -1
  61. package/dist/http/non-idempotent-mutations.cjs +38 -0
  62. package/dist/http/non-idempotent-mutations.cjs.map +1 -0
  63. package/dist/http/non-idempotent-mutations.d.cts +25 -0
  64. package/dist/http/non-idempotent-mutations.d.cts.map +1 -0
  65. package/dist/http/non-idempotent-mutations.d.ts +25 -0
  66. package/dist/http/non-idempotent-mutations.d.ts.map +1 -0
  67. package/dist/http/non-idempotent-mutations.js +35 -0
  68. package/dist/http/non-idempotent-mutations.js.map +1 -0
  69. package/dist/http/transport.cjs +9 -8
  70. package/dist/http/transport.cjs.map +1 -1
  71. package/dist/http/transport.d.cts +5 -0
  72. package/dist/http/transport.d.cts.map +1 -1
  73. package/dist/http/transport.d.ts +5 -0
  74. package/dist/http/transport.d.ts.map +1 -1
  75. package/dist/http/transport.js +9 -8
  76. package/dist/http/transport.js.map +1 -1
  77. package/dist/http/user-agent.cjs +12 -1
  78. package/dist/http/user-agent.cjs.map +1 -1
  79. package/dist/http/user-agent.d.cts +8 -0
  80. package/dist/http/user-agent.d.cts.map +1 -1
  81. package/dist/http/user-agent.d.ts +8 -0
  82. package/dist/http/user-agent.d.ts.map +1 -1
  83. package/dist/http/user-agent.js +13 -3
  84. package/dist/http/user-agent.js.map +1 -1
  85. package/dist/index.cjs +25 -0
  86. package/dist/index.d.cts +10 -9
  87. package/dist/index.d.cts.map +1 -1
  88. package/dist/index.d.ts +10 -9
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +7 -6
  91. package/dist/notifications/index.cjs +3 -0
  92. package/dist/notifications/index.d.cts +2 -0
  93. package/dist/notifications/index.d.cts.map +1 -1
  94. package/dist/notifications/index.d.ts +2 -0
  95. package/dist/notifications/index.d.ts.map +1 -1
  96. package/dist/notifications/index.js +2 -1
  97. package/dist/object.cjs +12 -1
  98. package/dist/object.cjs.map +1 -1
  99. package/dist/object.d.cts +7 -1
  100. package/dist/object.d.cts.map +1 -1
  101. package/dist/object.d.ts +7 -1
  102. package/dist/object.d.ts.map +1 -1
  103. package/dist/object.js +12 -1
  104. package/dist/object.js.map +1 -1
  105. package/dist/partner/auth-core.cjs +4 -23
  106. package/dist/partner/auth-core.cjs.map +1 -1
  107. package/dist/partner/auth-core.d.cts +1 -1
  108. package/dist/partner/auth-core.d.cts.map +1 -1
  109. package/dist/partner/auth-core.d.ts +1 -1
  110. package/dist/partner/auth-core.d.ts.map +1 -1
  111. package/dist/partner/auth-core.js +5 -24
  112. package/dist/partner/auth-core.js.map +1 -1
  113. package/dist/partner/client.cjs +25 -10
  114. package/dist/partner/client.cjs.map +1 -1
  115. package/dist/partner/client.d.cts +32 -10
  116. package/dist/partner/client.d.cts.map +1 -1
  117. package/dist/partner/client.d.ts +32 -10
  118. package/dist/partner/client.d.ts.map +1 -1
  119. package/dist/partner/client.js +25 -10
  120. package/dist/partner/client.js.map +1 -1
  121. package/dist/partner/endpoint-url.cjs +1 -0
  122. package/dist/partner/endpoint-url.cjs.map +1 -1
  123. package/dist/partner/endpoint-url.d.cts +2 -0
  124. package/dist/partner/endpoint-url.d.cts.map +1 -1
  125. package/dist/partner/endpoint-url.d.ts +2 -0
  126. package/dist/partner/endpoint-url.d.ts.map +1 -1
  127. package/dist/partner/endpoint-url.js +1 -0
  128. package/dist/partner/endpoint-url.js.map +1 -1
  129. package/dist/partner/in-memory.cjs.map +1 -1
  130. package/dist/partner/in-memory.js.map +1 -1
  131. package/dist/partner/index.cjs +9 -0
  132. package/dist/partner/index.d.cts +11 -2
  133. package/dist/partner/index.d.cts.map +1 -1
  134. package/dist/partner/index.d.ts +11 -2
  135. package/dist/partner/index.d.ts.map +1 -1
  136. package/dist/partner/index.js +3 -1
  137. package/dist/partner/raw.cjs +52 -44
  138. package/dist/partner/raw.cjs.map +1 -1
  139. package/dist/partner/raw.d.cts +16 -19
  140. package/dist/partner/raw.d.cts.map +1 -1
  141. package/dist/partner/raw.d.ts +16 -19
  142. package/dist/partner/raw.d.ts.map +1 -1
  143. package/dist/partner/raw.js +52 -44
  144. package/dist/partner/raw.js.map +1 -1
  145. package/dist/partner/redaction.cjs +133 -108
  146. package/dist/partner/redaction.cjs.map +1 -1
  147. package/dist/partner/redaction.d.cts +47 -13
  148. package/dist/partner/redaction.d.cts.map +1 -1
  149. package/dist/partner/redaction.d.ts +47 -13
  150. package/dist/partner/redaction.d.ts.map +1 -1
  151. package/dist/partner/redaction.js +133 -109
  152. package/dist/partner/redaction.js.map +1 -1
  153. package/dist/raw/encoding.cjs +29 -10
  154. package/dist/raw/encoding.cjs.map +1 -1
  155. package/dist/raw/encoding.d.cts +3 -2
  156. package/dist/raw/encoding.d.cts.map +1 -1
  157. package/dist/raw/encoding.d.ts +3 -2
  158. package/dist/raw/encoding.d.ts.map +1 -1
  159. package/dist/raw/encoding.js +29 -10
  160. package/dist/raw/encoding.js.map +1 -1
  161. package/dist/raw/index.cjs +64 -39
  162. package/dist/raw/index.cjs.map +1 -1
  163. package/dist/raw/index.d.cts +21 -19
  164. package/dist/raw/index.d.cts.map +1 -1
  165. package/dist/raw/index.d.ts +21 -19
  166. package/dist/raw/index.d.ts.map +1 -1
  167. package/dist/raw/index.js +47 -40
  168. package/dist/raw/index.js.map +1 -1
  169. package/dist/raw/url.cjs.map +1 -1
  170. package/dist/raw/url.d.cts +3 -3
  171. package/dist/raw/url.d.ts +3 -3
  172. package/dist/raw/url.js.map +1 -1
  173. package/dist/simulator/index.cjs +456 -125
  174. package/dist/simulator/index.cjs.map +1 -1
  175. package/dist/simulator/index.d.cts +16 -1
  176. package/dist/simulator/index.d.cts.map +1 -1
  177. package/dist/simulator/index.d.ts +16 -1
  178. package/dist/simulator/index.d.ts.map +1 -1
  179. package/dist/simulator/index.js +449 -130
  180. package/dist/simulator/index.js.map +1 -1
  181. package/dist/simulator/partner-capabilities.cjs +77 -0
  182. package/dist/simulator/partner-capabilities.cjs.map +1 -0
  183. package/dist/simulator/partner-capabilities.d.cts +53 -0
  184. package/dist/simulator/partner-capabilities.d.cts.map +1 -0
  185. package/dist/simulator/partner-capabilities.d.ts +53 -0
  186. package/dist/simulator/partner-capabilities.d.ts.map +1 -0
  187. package/dist/simulator/partner-capabilities.js +73 -0
  188. package/dist/simulator/partner-capabilities.js.map +1 -0
  189. package/dist/simulator/partner.cjs +55 -61
  190. package/dist/simulator/partner.cjs.map +1 -1
  191. package/dist/simulator/partner.d.cts +5 -2
  192. package/dist/simulator/partner.d.cts.map +1 -1
  193. package/dist/simulator/partner.d.ts +5 -2
  194. package/dist/simulator/partner.d.ts.map +1 -1
  195. package/dist/simulator/partner.js +56 -61
  196. package/dist/simulator/partner.js.map +1 -1
  197. package/dist/simulator/validation.cjs +160 -30
  198. package/dist/simulator/validation.cjs.map +1 -1
  199. package/dist/simulator/validation.d.cts +28 -6
  200. package/dist/simulator/validation.d.cts.map +1 -1
  201. package/dist/simulator/validation.d.ts +28 -6
  202. package/dist/simulator/validation.d.ts.map +1 -1
  203. package/dist/simulator/validation.js +148 -31
  204. package/dist/simulator/validation.js.map +1 -1
  205. package/dist/streams/file-source.cjs +1 -1
  206. package/dist/streams/file-source.cjs.map +1 -1
  207. package/dist/streams/file-source.js +1 -1
  208. package/dist/streams/file-source.js.map +1 -1
  209. package/dist/streams/hash.cjs +246 -13
  210. package/dist/streams/hash.cjs.map +1 -1
  211. package/dist/streams/hash.d.cts +37 -12
  212. package/dist/streams/hash.d.cts.map +1 -1
  213. package/dist/streams/hash.d.ts +37 -12
  214. package/dist/streams/hash.d.ts.map +1 -1
  215. package/dist/streams/hash.js +246 -15
  216. package/dist/streams/hash.js.map +1 -1
  217. package/dist/streams/index.cjs +2 -0
  218. package/dist/streams/index.d.cts +1 -1
  219. package/dist/streams/index.d.cts.map +1 -1
  220. package/dist/streams/index.d.ts +1 -1
  221. package/dist/streams/index.d.ts.map +1 -1
  222. package/dist/streams/index.js +2 -2
  223. package/dist/streams/progress.cjs +63 -4
  224. package/dist/streams/progress.cjs.map +1 -1
  225. package/dist/streams/progress.d.cts +28 -2
  226. package/dist/streams/progress.d.cts.map +1 -1
  227. package/dist/streams/progress.d.ts +28 -2
  228. package/dist/streams/progress.d.ts.map +1 -1
  229. package/dist/streams/progress.js +63 -4
  230. package/dist/streams/progress.js.map +1 -1
  231. package/dist/sync/download-staging.cjs +147 -35
  232. package/dist/sync/download-staging.cjs.map +1 -1
  233. package/dist/sync/download-staging.d.cts +25 -2
  234. package/dist/sync/download-staging.d.cts.map +1 -1
  235. package/dist/sync/download-staging.d.ts +25 -2
  236. package/dist/sync/download-staging.d.ts.map +1 -1
  237. package/dist/sync/download-staging.js +146 -36
  238. package/dist/sync/download-staging.js.map +1 -1
  239. package/dist/sync/index.d.cts +1 -1
  240. package/dist/sync/index.d.cts.map +1 -1
  241. package/dist/sync/index.d.ts +1 -1
  242. package/dist/sync/index.d.ts.map +1 -1
  243. package/dist/sync/local-file-io.cjs +5 -1
  244. package/dist/sync/local-file-io.cjs.map +1 -1
  245. package/dist/sync/local-file-io.d.cts +2 -0
  246. package/dist/sync/local-file-io.d.cts.map +1 -1
  247. package/dist/sync/local-file-io.d.ts +2 -0
  248. package/dist/sync/local-file-io.d.ts.map +1 -1
  249. package/dist/sync/local-file-io.js +5 -1
  250. package/dist/sync/local-file-io.js.map +1 -1
  251. package/dist/sync/pairing.cjs +1 -0
  252. package/dist/sync/pairing.cjs.map +1 -1
  253. package/dist/sync/pairing.js +1 -0
  254. package/dist/sync/pairing.js.map +1 -1
  255. package/dist/sync/scanners/b2.cjs +2 -0
  256. package/dist/sync/scanners/b2.cjs.map +1 -1
  257. package/dist/sync/scanners/b2.d.cts.map +1 -1
  258. package/dist/sync/scanners/b2.d.ts.map +1 -1
  259. package/dist/sync/scanners/b2.js +2 -0
  260. package/dist/sync/scanners/b2.js.map +1 -1
  261. package/dist/sync/scanners/local.cjs +150 -33
  262. package/dist/sync/scanners/local.cjs.map +1 -1
  263. package/dist/sync/scanners/local.d.cts +6 -0
  264. package/dist/sync/scanners/local.d.cts.map +1 -1
  265. package/dist/sync/scanners/local.d.ts +6 -0
  266. package/dist/sync/scanners/local.d.ts.map +1 -1
  267. package/dist/sync/scanners/local.js +150 -33
  268. package/dist/sync/scanners/local.js.map +1 -1
  269. package/dist/sync/synchronizer.cjs +76 -29
  270. package/dist/sync/synchronizer.cjs.map +1 -1
  271. package/dist/sync/synchronizer.d.cts +1 -0
  272. package/dist/sync/synchronizer.d.cts.map +1 -1
  273. package/dist/sync/synchronizer.d.ts +1 -0
  274. package/dist/sync/synchronizer.d.ts.map +1 -1
  275. package/dist/sync/synchronizer.js +76 -29
  276. package/dist/sync/synchronizer.js.map +1 -1
  277. package/dist/sync/types.d.cts +19 -3
  278. package/dist/sync/types.d.cts.map +1 -1
  279. package/dist/sync/types.d.ts +19 -3
  280. package/dist/sync/types.d.ts.map +1 -1
  281. package/dist/types/backup.d.cts +6 -1
  282. package/dist/types/backup.d.cts.map +1 -1
  283. package/dist/types/backup.d.ts +6 -1
  284. package/dist/types/backup.d.ts.map +1 -1
  285. package/dist/types/bucket.cjs +81 -11
  286. package/dist/types/bucket.cjs.map +1 -1
  287. package/dist/types/bucket.d.cts +190 -39
  288. package/dist/types/bucket.d.cts.map +1 -1
  289. package/dist/types/bucket.d.ts +190 -39
  290. package/dist/types/bucket.d.ts.map +1 -1
  291. package/dist/types/bucket.js +67 -12
  292. package/dist/types/bucket.js.map +1 -1
  293. package/dist/types/download.cjs.map +1 -1
  294. package/dist/types/download.d.cts +3 -17
  295. package/dist/types/download.d.cts.map +1 -1
  296. package/dist/types/download.d.ts +3 -17
  297. package/dist/types/download.d.ts.map +1 -1
  298. package/dist/types/download.js.map +1 -1
  299. package/dist/types/encryption.cjs +18 -0
  300. package/dist/types/encryption.cjs.map +1 -1
  301. package/dist/types/encryption.d.cts +21 -11
  302. package/dist/types/encryption.d.cts.map +1 -1
  303. package/dist/types/encryption.d.ts +21 -11
  304. package/dist/types/encryption.d.ts.map +1 -1
  305. package/dist/types/encryption.js +18 -0
  306. package/dist/types/encryption.js.map +1 -1
  307. package/dist/types/file.cjs +3 -0
  308. package/dist/types/file.cjs.map +1 -1
  309. package/dist/types/file.d.cts +125 -16
  310. package/dist/types/file.d.cts.map +1 -1
  311. package/dist/types/file.d.ts +125 -16
  312. package/dist/types/file.d.ts.map +1 -1
  313. package/dist/types/file.js +3 -1
  314. package/dist/types/file.js.map +1 -1
  315. package/dist/types/index.d.cts +7 -7
  316. package/dist/types/index.d.cts.map +1 -1
  317. package/dist/types/index.d.ts +7 -7
  318. package/dist/types/index.d.ts.map +1 -1
  319. package/dist/types/key.d.cts +4 -3
  320. package/dist/types/key.d.cts.map +1 -1
  321. package/dist/types/key.d.ts +4 -3
  322. package/dist/types/key.d.ts.map +1 -1
  323. package/dist/types/notifications.cjs +30 -0
  324. package/dist/types/notifications.cjs.map +1 -1
  325. package/dist/types/notifications.d.cts +26 -1
  326. package/dist/types/notifications.d.cts.map +1 -1
  327. package/dist/types/notifications.d.ts +26 -1
  328. package/dist/types/notifications.d.ts.map +1 -1
  329. package/dist/types/notifications.js +29 -1
  330. package/dist/types/notifications.js.map +1 -1
  331. package/dist/types/partner.cjs.map +1 -1
  332. package/dist/types/partner.d.cts +28 -36
  333. package/dist/types/partner.d.cts.map +1 -1
  334. package/dist/types/partner.d.ts +28 -36
  335. package/dist/types/partner.d.ts.map +1 -1
  336. package/dist/types/partner.js.map +1 -1
  337. package/dist/types/upload.d.cts +4 -0
  338. package/dist/types/upload.d.cts.map +1 -1
  339. package/dist/types/upload.d.ts +4 -0
  340. package/dist/types/upload.d.ts.map +1 -1
  341. package/dist/upload/large.cjs +10 -7
  342. package/dist/upload/large.cjs.map +1 -1
  343. package/dist/upload/large.d.cts +15 -6
  344. package/dist/upload/large.d.cts.map +1 -1
  345. package/dist/upload/large.d.ts +15 -6
  346. package/dist/upload/large.d.ts.map +1 -1
  347. package/dist/upload/large.js +10 -7
  348. package/dist/upload/large.js.map +1 -1
  349. package/dist/upload/options.cjs.map +1 -1
  350. package/dist/upload/options.d.cts +12 -0
  351. package/dist/upload/options.d.cts.map +1 -1
  352. package/dist/upload/options.d.ts +12 -0
  353. package/dist/upload/options.d.ts.map +1 -1
  354. package/dist/upload/options.js.map +1 -1
  355. package/dist/upload/resume.cjs +4 -3
  356. package/dist/upload/resume.cjs.map +1 -1
  357. package/dist/upload/resume.d.cts +6 -4
  358. package/dist/upload/resume.d.cts.map +1 -1
  359. package/dist/upload/resume.d.ts +6 -4
  360. package/dist/upload/resume.d.ts.map +1 -1
  361. package/dist/upload/resume.js +4 -3
  362. package/dist/upload/resume.js.map +1 -1
  363. package/dist/upload/retry.cjs +17 -10
  364. package/dist/upload/retry.cjs.map +1 -1
  365. package/dist/upload/retry.d.cts.map +1 -1
  366. package/dist/upload/retry.d.ts.map +1 -1
  367. package/dist/upload/retry.js +17 -10
  368. package/dist/upload/retry.js.map +1 -1
  369. package/dist/upload/single.cjs +35 -31
  370. package/dist/upload/single.cjs.map +1 -1
  371. package/dist/upload/single.d.cts +6 -0
  372. package/dist/upload/single.d.cts.map +1 -1
  373. package/dist/upload/single.d.ts +6 -0
  374. package/dist/upload/single.d.ts.map +1 -1
  375. package/dist/upload/single.js +35 -31
  376. package/dist/upload/single.js.map +1 -1
  377. package/dist/upload/stream.cjs +12 -1
  378. package/dist/upload/stream.cjs.map +1 -1
  379. package/dist/upload/stream.d.cts +6 -0
  380. package/dist/upload/stream.d.cts.map +1 -1
  381. package/dist/upload/stream.d.ts +6 -0
  382. package/dist/upload/stream.d.ts.map +1 -1
  383. package/dist/upload/stream.js +12 -1
  384. package/dist/upload/stream.js.map +1 -1
  385. package/dist/version.cjs +56 -4
  386. package/dist/version.cjs.map +1 -1
  387. package/dist/version.d.cts +53 -4
  388. package/dist/version.d.cts.map +1 -1
  389. package/dist/version.d.ts +53 -4
  390. package/dist/version.d.ts.map +1 -1
  391. package/dist/version.js +52 -5
  392. package/dist/version.js.map +1 -1
  393. package/package.json +11 -4
@@ -1 +1 @@
1
- {"version":3,"file":"b2.cjs","names":[],"sources":["../../../src/sync/scanners/b2.ts"],"sourcesContent":["import type { Bucket } from '../../bucket.ts'\nimport { FileAction, type FileVersion } from '../../types/file.ts'\nimport type { FileId } from '../../types/ids.ts'\nimport { isSignalAbortError } from '../../util/abort.ts'\nimport { sanitizeErrorReason } from '../../util/error-reason.ts'\nimport {\n literalPrefixForSyncFilters,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { compareCodeUnits, compareSyncRelativePaths } from '../path-order.ts'\nimport { assertSyncPathAllowed } from '../paths.ts'\nimport {\n asRawB2KeyPrefix,\n b2KeyToRelativePathUnderPrefix,\n localFilesystemCanonicalSyncPath,\n localFilesystemSyncPathIsUnsafe,\n} from '../prefix.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport { selectB2ComparableSha1, syncSha1StateOf } from '../sha1-metadata.ts'\nimport type {\n B2SyncPath,\n SyncErrorEvent,\n SyncFolder,\n SyncScanOptions,\n SyncSkipReason,\n} from '../types.ts'\n\nconst MAX_EMPTY_B2_SCAN_PAGES = 100\n\ninterface B2ScanEntry {\n relativePath: string\n versions: FileVersion[]\n}\n\ninterface VisibleB2ScanEntry extends B2ScanEntry {\n fileName: string\n selectedVersion: FileVersion\n}\n\n/**\n * Scans a B2 bucket (optionally filtered by a raw B2 key prefix) and yields\n * {@link B2SyncPath} entries sorted by `compareSyncRelativePaths(relativePath)`. Hidden files are excluded.\n * Raw B2 file names are used only as an internal tie-breaker after collision handling.\n * All versions for the listed prefix are fetched, grouped, and sorted before\n * yielding; exclude filters are applied client-side and do not reduce that\n * B2 listing memory footprint.\n * SDK-reserved temporary names fail the scan because syncing them could corrupt\n * in-progress transfers.\n */\nexport class B2Folder implements SyncFolder {\n readonly type = 'b2' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Raw B2 key prefix this folder scans, preserving caller-provided separators verbatim. */\n readonly rawPrefix: string\n private readonly bucket: Bucket\n\n /**\n * Creates a new B2Folder for the given bucket and optional prefix.\n * @param bucket - The B2 bucket to scan.\n * @param prefix - Optional raw B2 key prefix to restrict the scan scope.\n * Backslashes are preserved as raw B2 key characters; pass `/` explicitly for slash prefixes.\n */\n constructor(bucket: Bucket, prefix = '') {\n this.bucket = bucket\n this.rawPrefix = asRawB2KeyPrefix(prefix)\n }\n\n /**\n * Lists all file versions in the bucket, groups by name, and yields the latest visible version.\n * @param options - Optional scan controls.\n */\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pagination, grouping, and filtering stay in one async iterator.\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<B2SyncPath> {\n validateSyncFilters(options)\n const maxScanEntries = scanEntryLimit(options)\n const grouped = new Map<string, B2ScanEntry>()\n const listPrefix = this.listPrefixFor(options)\n\n let listedVersions = 0\n let startFileName: string | undefined\n let startFileId: FileId | undefined\n let emptyPageCount = 0\n\n while (true) {\n if (scanIsAborted(options)) return\n\n let listing: Awaited<ReturnType<Bucket['listFileVersions']>>\n try {\n listing = await this.bucket.listFileVersions({\n ...(listPrefix !== '' ? { prefix: listPrefix } : {}),\n ...(startFileName !== undefined ? { startFileName } : {}),\n ...(startFileId !== undefined ? { startFileId } : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n })\n } catch (err) {\n if (isSignalAbortError(options.signal, err)) return\n throw emitScanError(options, 'failed to scan B2 file versions', err)\n }\n if (scanIsAborted(options)) return\n\n if (listing.files.length === 0) {\n emptyPageCount++\n if (emptyPageCount > MAX_EMPTY_B2_SCAN_PAGES) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination returned too many empty pages'),\n )\n }\n } else {\n emptyPageCount = 0\n }\n\n for (const fv of listing.files) {\n if (scanIsAborted(options)) return\n assertScanEntryLimit(listedVersions + 1, maxScanEntries)\n listedVersions++\n\n // Real B2 honors the prefix in listFileVersions, but custom\n // transports and the simulator can over-return. Guard before\n // stripping the raw prefix so relativePath is never corrupted.\n if (this.rawPrefix !== '' && !fv.fileName.startsWith(this.rawPrefix)) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'outside-prefix',\n `listed object is outside configured B2 prefix ${JSON.stringify(this.rawPrefix)}`,\n )\n continue\n }\n\n if (options.requireLocalSafePaths === true && fv.fileName.includes('\\\\')) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'local-unsafe-name',\n 'object name contains a backslash that is unsafe for local filesystem destinations',\n )\n continue\n }\n\n const relativePath = this.tryToRelativePath(fv.fileName)\n if (relativePath === null) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'unsafe-name',\n 'object name cannot be represented as a safe sync relative path',\n )\n continue\n }\n\n if (!pathPassesSyncFilters(relativePath, options)) {\n if (pathSkippedByRegExpInputLimit(relativePath, options)) {\n emitScannerSkip(options, {\n ...regexpInputTooLongSkip(relativePath),\n b2FileName: fv.fileName,\n })\n }\n continue\n }\n\n const existing = grouped.get(fv.fileName)\n if (existing) {\n existing.versions.push(fv)\n } else {\n grouped.set(fv.fileName, { relativePath, versions: [fv] })\n }\n }\n\n if (!listing.nextFileName) break\n if (\n listing.nextFileName === startFileName &&\n (listing.nextFileId ?? undefined) === startFileId\n ) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination did not advance'),\n )\n }\n startFileName = listing.nextFileName\n startFileId = listing.nextFileId ?? undefined\n }\n\n const visible = this.visibleCandidates(grouped, options)\n const withoutRelativeCollisions = this.rejectRelativePathCollisions(visible, options)\n const safeCandidates =\n options.requireLocalSafePaths === true\n ? this.rejectLocalPathCollisions(withoutRelativeCollisions, options)\n : withoutRelativeCollisions\n const sorted = safeCandidates.sort(\n (a, b) =>\n compareSyncRelativePaths(a.relativePath, b.relativePath) ||\n compareCodeUnits(a.fileName, b.fileName),\n )\n\n for (const { relativePath, versions, selectedVersion } of sorted) {\n if (scanIsAborted(options)) return\n // Reserved SDK temp names are a hard safety failure, not an ordinary filter.\n assertSyncPathAllowed(relativePath)\n const contentSha1 = selectB2ComparableSha1(selectedVersion)\n yield {\n relativePath,\n modTimeMillis: selectedVersion.uploadTimestamp,\n size: selectedVersion.contentLength,\n contentSha1,\n contentSha1State: syncSha1StateOf({ contentSha1 }),\n selectedVersion,\n allVersions: versions,\n }\n }\n }\n\n private tryToRelativePath(fileName: string): string | null {\n try {\n return b2KeyToRelativePathUnderPrefix(this.rawPrefix, fileName)\n } catch {\n return null\n }\n }\n\n private listPrefixFor(filters: SyncScanOptions | undefined): string {\n const filterPrefix = literalPrefixForSyncFilters(filters)\n if (filterPrefix === '') return this.rawPrefix\n if (this.rawPrefix !== '' && !this.rawPrefix.endsWith('/')) return this.rawPrefix\n return `${this.rawPrefix}${rawPrefixBeforeNormalizedSeparator(filterPrefix)}`\n }\n\n private visibleCandidates(\n grouped: Map<string, B2ScanEntry>,\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const visible: VisibleB2ScanEntry[] = []\n\n for (const [fileName, entry] of grouped) {\n entry.versions.sort((a, b) => b.uploadTimestamp - a.uploadTimestamp)\n const selected = entry.versions[0]\n if (!selected || selected.action === FileAction.Hide) continue\n\n if (\n filters?.requireLocalSafePaths === true &&\n localFilesystemSyncPathIsUnsafe(entry.relativePath)\n ) {\n this.emitSkip(\n filters,\n entry.relativePath,\n fileName,\n 'local-unsafe-name',\n 'object name is unsafe for a local filesystem destination',\n )\n continue\n }\n\n visible.push({\n fileName,\n relativePath: entry.relativePath,\n versions: entry.versions,\n selectedVersion: selected,\n })\n }\n\n return visible\n }\n\n private rejectRelativePathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedRelativePaths = new Set<string>()\n\n for (const candidate of candidates) {\n if (collidedRelativePaths.has(candidate.relativePath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n 'object normalizes to a relative path already rejected because of another raw B2 key',\n )\n continue\n }\n\n const owner = owners.get(candidate.relativePath)\n if (owner !== undefined && owner.fileName !== candidate.fileName) {\n owners.delete(candidate.relativePath)\n removeAcceptedCandidate(accepted, owner)\n collidedRelativePaths.add(candidate.relativePath)\n this.emitSkip(\n filters,\n candidate.relativePath,\n owner.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(candidate.fileName)}`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(owner.fileName)}`,\n )\n continue\n }\n\n owners.set(candidate.relativePath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private rejectLocalPathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedLocalPaths = new Set<string>()\n\n for (const candidate of candidates) {\n const canonicalPath = localFilesystemCanonicalSyncPath(candidate.relativePath)\n if (collidedLocalPaths.has(canonicalPath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n 'object collides with another object on case-insensitive or Unicode-normalizing filesystems',\n )\n continue\n }\n\n const owner = owners.get(canonicalPath)\n if (owner !== undefined) {\n owners.delete(canonicalPath)\n removeAcceptedCandidate(accepted, owner)\n collidedLocalPaths.add(canonicalPath)\n this.emitSkip(\n filters,\n owner.relativePath,\n owner.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(candidate.fileName)} on local filesystems`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(owner.fileName)} on local filesystems`,\n )\n continue\n }\n\n owners.set(canonicalPath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private emitSkip(\n filters: SyncScanOptions | undefined,\n path: string,\n b2FileName: string,\n reason: SyncSkipReason,\n message: string,\n ): void {\n emitScannerSkip(filters, {\n type: 'skip',\n path,\n size: 0,\n message: `Skipped B2 object ${JSON.stringify(b2FileName)}: ${message}`,\n reason,\n b2FileName,\n })\n }\n}\n\nfunction rawPrefixBeforeNormalizedSeparator(filterPrefix: string): string {\n const separatorIndex = filterPrefix.indexOf('/')\n return separatorIndex === -1 ? filterPrefix : filterPrefix.slice(0, separatorIndex)\n}\n\nfunction emitScanError(options: SyncScanOptions, message: string, err: unknown): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path: '',\n size: 0,\n message: `${message}: ${sanitizeErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n}\n\nfunction removeAcceptedCandidate(\n candidates: VisibleB2ScanEntry[],\n target: VisibleB2ScanEntry,\n): void {\n const index = candidates.indexOf(target)\n if (index !== -1) candidates.splice(index, 1)\n}\n\nfunction scanIsAborted(filters: SyncScanOptions | undefined): boolean {\n return filters?.signal?.aborted === true\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"b2.cjs","names":[],"sources":["../../../src/sync/scanners/b2.ts"],"sourcesContent":["import type { Bucket } from '../../bucket.ts'\nimport {\n FileAction,\n type FileVersion,\n type ListedConcreteFileVersion,\n type ListFileVersionsResponse,\n} from '../../types/file.ts'\nimport type { FileId } from '../../types/ids.ts'\nimport { isSignalAbortError } from '../../util/abort.ts'\nimport { sanitizeErrorReason } from '../../util/error-reason.ts'\nimport {\n literalPrefixForSyncFilters,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { compareCodeUnits, compareSyncRelativePaths } from '../path-order.ts'\nimport { assertSyncPathAllowed } from '../paths.ts'\nimport {\n asRawB2KeyPrefix,\n b2KeyToRelativePathUnderPrefix,\n localFilesystemCanonicalSyncPath,\n localFilesystemSyncPathIsUnsafe,\n} from '../prefix.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport { selectB2ComparableSha1, syncSha1StateOf } from '../sha1-metadata.ts'\nimport type {\n B2SyncPath,\n SyncErrorEvent,\n SyncFolder,\n SyncScanOptions,\n SyncSkipReason,\n} from '../types.ts'\n\nconst MAX_EMPTY_B2_SCAN_PAGES = 100\n\ninterface B2ScanEntry {\n relativePath: string\n versions: ListedConcreteFileVersion[]\n}\n\ninterface VisibleB2ScanEntry extends B2ScanEntry {\n fileName: string\n selectedVersion: FileVersion\n}\n\n/**\n * Scans a B2 bucket (optionally filtered by a raw B2 key prefix) and yields\n * {@link B2SyncPath} entries sorted by `compareSyncRelativePaths(relativePath)`. Hidden files are excluded.\n * Raw B2 file names are used only as an internal tie-breaker after collision handling.\n * All versions for the listed prefix are fetched, grouped, and sorted before\n * yielding; exclude filters are applied client-side and do not reduce that\n * B2 listing memory footprint.\n * SDK-reserved temporary names fail the scan because syncing them could corrupt\n * in-progress transfers.\n */\nexport class B2Folder implements SyncFolder {\n readonly type = 'b2' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Raw B2 key prefix this folder scans, preserving caller-provided separators verbatim. */\n readonly rawPrefix: string\n private readonly bucket: Bucket\n\n /**\n * Creates a new B2Folder for the given bucket and optional prefix.\n * @param bucket - The B2 bucket to scan.\n * @param prefix - Optional raw B2 key prefix to restrict the scan scope.\n * Backslashes are preserved as raw B2 key characters; pass `/` explicitly for slash prefixes.\n */\n constructor(bucket: Bucket, prefix = '') {\n this.bucket = bucket\n this.rawPrefix = asRawB2KeyPrefix(prefix)\n }\n\n /**\n * Lists all file versions in the bucket, groups by name, and yields the latest visible version.\n * @param options - Optional scan controls.\n */\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pagination, grouping, and filtering stay in one async iterator.\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<B2SyncPath> {\n validateSyncFilters(options)\n const maxScanEntries = scanEntryLimit(options)\n const grouped = new Map<string, B2ScanEntry>()\n const listPrefix = this.listPrefixFor(options)\n\n let listedVersions = 0\n let startFileName: string | undefined\n let startFileId: FileId | undefined\n let emptyPageCount = 0\n\n while (true) {\n if (scanIsAborted(options)) return\n\n let listing: ListFileVersionsResponse\n try {\n listing = await this.bucket.listFileVersions({\n ...(listPrefix !== '' ? { prefix: listPrefix } : {}),\n ...(startFileName !== undefined ? { startFileName } : {}),\n ...(startFileId !== undefined ? { startFileId } : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n })\n } catch (err) {\n if (isSignalAbortError(options.signal, err)) return\n throw emitScanError(options, 'failed to scan B2 file versions', err)\n }\n if (scanIsAborted(options)) return\n\n if (listing.files.length === 0) {\n emptyPageCount++\n if (emptyPageCount > MAX_EMPTY_B2_SCAN_PAGES) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination returned too many empty pages'),\n )\n }\n } else {\n emptyPageCount = 0\n }\n\n for (const fv of listing.files) {\n if (scanIsAborted(options)) return\n assertScanEntryLimit(listedVersions + 1, maxScanEntries)\n listedVersions++\n\n const runtimeFile = fv as { readonly action?: unknown; readonly fileId?: unknown }\n if (runtimeFile.action === FileAction.Folder || typeof runtimeFile.fileId !== 'string') {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error(\n 'B2 listFileVersions returned a delimiter folder row or row without a string fileId even though the scanner did not request delimiter grouping',\n ),\n )\n }\n\n // Real B2 honors the prefix in listFileVersions, but custom\n // transports and the simulator can over-return. Guard before\n // stripping the raw prefix so relativePath is never corrupted.\n if (this.rawPrefix !== '' && !fv.fileName.startsWith(this.rawPrefix)) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'outside-prefix',\n `listed object is outside configured B2 prefix ${JSON.stringify(this.rawPrefix)}`,\n )\n continue\n }\n\n if (options.requireLocalSafePaths === true && fv.fileName.includes('\\\\')) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'local-unsafe-name',\n 'object name contains a backslash that is unsafe for local filesystem destinations',\n )\n continue\n }\n\n const relativePath = this.tryToRelativePath(fv.fileName)\n if (relativePath === null) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'unsafe-name',\n 'object name cannot be represented as a safe sync relative path',\n )\n continue\n }\n\n if (!pathPassesSyncFilters(relativePath, options)) {\n if (pathSkippedByRegExpInputLimit(relativePath, options)) {\n emitScannerSkip(options, {\n ...regexpInputTooLongSkip(relativePath),\n b2FileName: fv.fileName,\n })\n }\n continue\n }\n\n const existing = grouped.get(fv.fileName)\n if (existing) {\n existing.versions.push(fv)\n } else {\n grouped.set(fv.fileName, { relativePath, versions: [fv] })\n }\n }\n\n if (!listing.nextFileName) break\n if (\n listing.nextFileName === startFileName &&\n (listing.nextFileId ?? undefined) === startFileId\n ) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination did not advance'),\n )\n }\n startFileName = listing.nextFileName\n startFileId = listing.nextFileId ?? undefined\n }\n\n const visible = this.visibleCandidates(grouped, options)\n const withoutRelativeCollisions = this.rejectRelativePathCollisions(visible, options)\n const safeCandidates =\n options.requireLocalSafePaths === true\n ? this.rejectLocalPathCollisions(withoutRelativeCollisions, options)\n : withoutRelativeCollisions\n const sorted = safeCandidates.sort(\n (a, b) =>\n compareSyncRelativePaths(a.relativePath, b.relativePath) ||\n compareCodeUnits(a.fileName, b.fileName),\n )\n\n for (const { relativePath, versions, selectedVersion } of sorted) {\n if (scanIsAborted(options)) return\n // Reserved SDK temp names are a hard safety failure, not an ordinary filter.\n assertSyncPathAllowed(relativePath)\n const contentSha1 = selectB2ComparableSha1(selectedVersion)\n yield {\n relativePath,\n modTimeMillis: selectedVersion.uploadTimestamp,\n size: selectedVersion.contentLength,\n contentSha1,\n contentSha1State: syncSha1StateOf({ contentSha1 }),\n selectedVersion,\n allVersions: versions,\n }\n }\n }\n\n private tryToRelativePath(fileName: string): string | null {\n try {\n return b2KeyToRelativePathUnderPrefix(this.rawPrefix, fileName)\n } catch {\n return null\n }\n }\n\n private listPrefixFor(filters: SyncScanOptions | undefined): string {\n const filterPrefix = literalPrefixForSyncFilters(filters)\n if (filterPrefix === '') return this.rawPrefix\n if (this.rawPrefix !== '' && !this.rawPrefix.endsWith('/')) return this.rawPrefix\n return `${this.rawPrefix}${rawPrefixBeforeNormalizedSeparator(filterPrefix)}`\n }\n\n private visibleCandidates(\n grouped: Map<string, B2ScanEntry>,\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const visible: VisibleB2ScanEntry[] = []\n\n for (const [fileName, entry] of grouped) {\n entry.versions.sort((a, b) => b.uploadTimestamp - a.uploadTimestamp)\n const selected = entry.versions[0]\n if (!selected || selected.action === FileAction.Hide) continue\n\n if (\n filters?.requireLocalSafePaths === true &&\n localFilesystemSyncPathIsUnsafe(entry.relativePath)\n ) {\n this.emitSkip(\n filters,\n entry.relativePath,\n fileName,\n 'local-unsafe-name',\n 'object name is unsafe for a local filesystem destination',\n )\n continue\n }\n\n visible.push({\n fileName,\n relativePath: entry.relativePath,\n versions: entry.versions,\n selectedVersion: selected,\n })\n }\n\n return visible\n }\n\n private rejectRelativePathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedRelativePaths = new Set<string>()\n\n for (const candidate of candidates) {\n if (collidedRelativePaths.has(candidate.relativePath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n 'object normalizes to a relative path already rejected because of another raw B2 key',\n )\n continue\n }\n\n const owner = owners.get(candidate.relativePath)\n if (owner !== undefined && owner.fileName !== candidate.fileName) {\n owners.delete(candidate.relativePath)\n removeAcceptedCandidate(accepted, owner)\n collidedRelativePaths.add(candidate.relativePath)\n this.emitSkip(\n filters,\n candidate.relativePath,\n owner.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(candidate.fileName)}`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(owner.fileName)}`,\n )\n continue\n }\n\n owners.set(candidate.relativePath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private rejectLocalPathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedLocalPaths = new Set<string>()\n\n for (const candidate of candidates) {\n const canonicalPath = localFilesystemCanonicalSyncPath(candidate.relativePath)\n if (collidedLocalPaths.has(canonicalPath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n 'object collides with another object on case-insensitive or Unicode-normalizing filesystems',\n )\n continue\n }\n\n const owner = owners.get(canonicalPath)\n if (owner !== undefined) {\n owners.delete(canonicalPath)\n removeAcceptedCandidate(accepted, owner)\n collidedLocalPaths.add(canonicalPath)\n this.emitSkip(\n filters,\n owner.relativePath,\n owner.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(candidate.fileName)} on local filesystems`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(owner.fileName)} on local filesystems`,\n )\n continue\n }\n\n owners.set(canonicalPath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private emitSkip(\n filters: SyncScanOptions | undefined,\n path: string,\n b2FileName: string,\n reason: SyncSkipReason,\n message: string,\n ): void {\n emitScannerSkip(filters, {\n type: 'skip',\n path,\n size: 0,\n message: `Skipped B2 object ${JSON.stringify(b2FileName)}: ${message}`,\n reason,\n b2FileName,\n })\n }\n}\n\nfunction rawPrefixBeforeNormalizedSeparator(filterPrefix: string): string {\n const separatorIndex = filterPrefix.indexOf('/')\n return separatorIndex === -1 ? filterPrefix : filterPrefix.slice(0, separatorIndex)\n}\n\nfunction emitScanError(options: SyncScanOptions, message: string, err: unknown): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path: '',\n size: 0,\n message: `${message}: ${sanitizeErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n}\n\nfunction removeAcceptedCandidate(\n candidates: VisibleB2ScanEntry[],\n target: VisibleB2ScanEntry,\n): void {\n const index = candidates.indexOf(target)\n if (index !== -1) candidates.splice(index, 1)\n}\n\nfunction scanIsAborted(filters: SyncScanOptions | undefined): boolean {\n return filters?.signal?.aborted === true\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"b2.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/b2.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAsB7C,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAA;AAcpB;;;;;;;;;GASG;AACH,qBAAa,QAAS,YAAW,UAAU;IACzC,QAAQ,CAAC,IAAI,EAAG,IAAI,CAAS;IAC7B,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK;IAKvC;;;OAGG;IAEI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,UAAU,CAAC;IAiJtE,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,4BAA4B;IAiDpC,OAAO,CAAC,yBAAyB;IAkDjC,OAAO,CAAC,QAAQ;CAgBjB"}
1
+ {"version":3,"file":"b2.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/b2.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AA2B7C,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAA;AAcpB;;;;;;;;;GASG;AACH,qBAAa,QAAS,YAAW,UAAU;IACzC,QAAQ,CAAC,IAAI,EAAG,IAAI,CAAS;IAC7B,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK;IAKvC;;;OAGG;IAEI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,UAAU,CAAC;IA4JtE,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,4BAA4B;IAiDpC,OAAO,CAAC,yBAAyB;IAkDjC,OAAO,CAAC,QAAQ;CAgBjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"b2.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/b2.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAsB7C,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAA;AAcpB;;;;;;;;;GASG;AACH,qBAAa,QAAS,YAAW,UAAU;IACzC,QAAQ,CAAC,IAAI,EAAG,IAAI,CAAS;IAC7B,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK;IAKvC;;;OAGG;IAEI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,UAAU,CAAC;IAiJtE,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,4BAA4B;IAiDpC,OAAO,CAAC,yBAAyB;IAkDjC,OAAO,CAAC,QAAQ;CAgBjB"}
1
+ {"version":3,"file":"b2.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/b2.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AA2B7C,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAA;AAcpB;;;;;;;;;GASG;AACH,qBAAa,QAAS,YAAW,UAAU;IACzC,QAAQ,CAAC,IAAI,EAAG,IAAI,CAAS;IAC7B,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,MAAM,SAAK;IAKvC;;;OAGG;IAEI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,UAAU,CAAC;IA4JtE,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,4BAA4B;IAiDpC,OAAO,CAAC,yBAAyB;IAkDjC,OAAO,CAAC,QAAQ;CAgBjB"}
@@ -74,6 +74,8 @@ var B2Folder = class {
74
74
  if (scanIsAborted(options)) return;
75
75
  assertScanEntryLimit(listedVersions + 1, maxScanEntries);
76
76
  listedVersions++;
77
+ const runtimeFile = fv;
78
+ if (runtimeFile.action === FileAction.Folder || typeof runtimeFile.fileId !== "string") throw emitScanError(options, "failed to scan B2 file versions", /* @__PURE__ */ new Error("B2 listFileVersions returned a delimiter folder row or row without a string fileId even though the scanner did not request delimiter grouping"));
77
79
  if (this.rawPrefix !== "" && !fv.fileName.startsWith(this.rawPrefix)) {
78
80
  this.emitSkip(options, fv.fileName, fv.fileName, "outside-prefix", `listed object is outside configured B2 prefix ${JSON.stringify(this.rawPrefix)}`);
79
81
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"b2.js","names":[],"sources":["../../../src/sync/scanners/b2.ts"],"sourcesContent":["import type { Bucket } from '../../bucket.ts'\nimport { FileAction, type FileVersion } from '../../types/file.ts'\nimport type { FileId } from '../../types/ids.ts'\nimport { isSignalAbortError } from '../../util/abort.ts'\nimport { sanitizeErrorReason } from '../../util/error-reason.ts'\nimport {\n literalPrefixForSyncFilters,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { compareCodeUnits, compareSyncRelativePaths } from '../path-order.ts'\nimport { assertSyncPathAllowed } from '../paths.ts'\nimport {\n asRawB2KeyPrefix,\n b2KeyToRelativePathUnderPrefix,\n localFilesystemCanonicalSyncPath,\n localFilesystemSyncPathIsUnsafe,\n} from '../prefix.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport { selectB2ComparableSha1, syncSha1StateOf } from '../sha1-metadata.ts'\nimport type {\n B2SyncPath,\n SyncErrorEvent,\n SyncFolder,\n SyncScanOptions,\n SyncSkipReason,\n} from '../types.ts'\n\nconst MAX_EMPTY_B2_SCAN_PAGES = 100\n\ninterface B2ScanEntry {\n relativePath: string\n versions: FileVersion[]\n}\n\ninterface VisibleB2ScanEntry extends B2ScanEntry {\n fileName: string\n selectedVersion: FileVersion\n}\n\n/**\n * Scans a B2 bucket (optionally filtered by a raw B2 key prefix) and yields\n * {@link B2SyncPath} entries sorted by `compareSyncRelativePaths(relativePath)`. Hidden files are excluded.\n * Raw B2 file names are used only as an internal tie-breaker after collision handling.\n * All versions for the listed prefix are fetched, grouped, and sorted before\n * yielding; exclude filters are applied client-side and do not reduce that\n * B2 listing memory footprint.\n * SDK-reserved temporary names fail the scan because syncing them could corrupt\n * in-progress transfers.\n */\nexport class B2Folder implements SyncFolder {\n readonly type = 'b2' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Raw B2 key prefix this folder scans, preserving caller-provided separators verbatim. */\n readonly rawPrefix: string\n private readonly bucket: Bucket\n\n /**\n * Creates a new B2Folder for the given bucket and optional prefix.\n * @param bucket - The B2 bucket to scan.\n * @param prefix - Optional raw B2 key prefix to restrict the scan scope.\n * Backslashes are preserved as raw B2 key characters; pass `/` explicitly for slash prefixes.\n */\n constructor(bucket: Bucket, prefix = '') {\n this.bucket = bucket\n this.rawPrefix = asRawB2KeyPrefix(prefix)\n }\n\n /**\n * Lists all file versions in the bucket, groups by name, and yields the latest visible version.\n * @param options - Optional scan controls.\n */\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pagination, grouping, and filtering stay in one async iterator.\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<B2SyncPath> {\n validateSyncFilters(options)\n const maxScanEntries = scanEntryLimit(options)\n const grouped = new Map<string, B2ScanEntry>()\n const listPrefix = this.listPrefixFor(options)\n\n let listedVersions = 0\n let startFileName: string | undefined\n let startFileId: FileId | undefined\n let emptyPageCount = 0\n\n while (true) {\n if (scanIsAborted(options)) return\n\n let listing: Awaited<ReturnType<Bucket['listFileVersions']>>\n try {\n listing = await this.bucket.listFileVersions({\n ...(listPrefix !== '' ? { prefix: listPrefix } : {}),\n ...(startFileName !== undefined ? { startFileName } : {}),\n ...(startFileId !== undefined ? { startFileId } : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n })\n } catch (err) {\n if (isSignalAbortError(options.signal, err)) return\n throw emitScanError(options, 'failed to scan B2 file versions', err)\n }\n if (scanIsAborted(options)) return\n\n if (listing.files.length === 0) {\n emptyPageCount++\n if (emptyPageCount > MAX_EMPTY_B2_SCAN_PAGES) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination returned too many empty pages'),\n )\n }\n } else {\n emptyPageCount = 0\n }\n\n for (const fv of listing.files) {\n if (scanIsAborted(options)) return\n assertScanEntryLimit(listedVersions + 1, maxScanEntries)\n listedVersions++\n\n // Real B2 honors the prefix in listFileVersions, but custom\n // transports and the simulator can over-return. Guard before\n // stripping the raw prefix so relativePath is never corrupted.\n if (this.rawPrefix !== '' && !fv.fileName.startsWith(this.rawPrefix)) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'outside-prefix',\n `listed object is outside configured B2 prefix ${JSON.stringify(this.rawPrefix)}`,\n )\n continue\n }\n\n if (options.requireLocalSafePaths === true && fv.fileName.includes('\\\\')) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'local-unsafe-name',\n 'object name contains a backslash that is unsafe for local filesystem destinations',\n )\n continue\n }\n\n const relativePath = this.tryToRelativePath(fv.fileName)\n if (relativePath === null) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'unsafe-name',\n 'object name cannot be represented as a safe sync relative path',\n )\n continue\n }\n\n if (!pathPassesSyncFilters(relativePath, options)) {\n if (pathSkippedByRegExpInputLimit(relativePath, options)) {\n emitScannerSkip(options, {\n ...regexpInputTooLongSkip(relativePath),\n b2FileName: fv.fileName,\n })\n }\n continue\n }\n\n const existing = grouped.get(fv.fileName)\n if (existing) {\n existing.versions.push(fv)\n } else {\n grouped.set(fv.fileName, { relativePath, versions: [fv] })\n }\n }\n\n if (!listing.nextFileName) break\n if (\n listing.nextFileName === startFileName &&\n (listing.nextFileId ?? undefined) === startFileId\n ) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination did not advance'),\n )\n }\n startFileName = listing.nextFileName\n startFileId = listing.nextFileId ?? undefined\n }\n\n const visible = this.visibleCandidates(grouped, options)\n const withoutRelativeCollisions = this.rejectRelativePathCollisions(visible, options)\n const safeCandidates =\n options.requireLocalSafePaths === true\n ? this.rejectLocalPathCollisions(withoutRelativeCollisions, options)\n : withoutRelativeCollisions\n const sorted = safeCandidates.sort(\n (a, b) =>\n compareSyncRelativePaths(a.relativePath, b.relativePath) ||\n compareCodeUnits(a.fileName, b.fileName),\n )\n\n for (const { relativePath, versions, selectedVersion } of sorted) {\n if (scanIsAborted(options)) return\n // Reserved SDK temp names are a hard safety failure, not an ordinary filter.\n assertSyncPathAllowed(relativePath)\n const contentSha1 = selectB2ComparableSha1(selectedVersion)\n yield {\n relativePath,\n modTimeMillis: selectedVersion.uploadTimestamp,\n size: selectedVersion.contentLength,\n contentSha1,\n contentSha1State: syncSha1StateOf({ contentSha1 }),\n selectedVersion,\n allVersions: versions,\n }\n }\n }\n\n private tryToRelativePath(fileName: string): string | null {\n try {\n return b2KeyToRelativePathUnderPrefix(this.rawPrefix, fileName)\n } catch {\n return null\n }\n }\n\n private listPrefixFor(filters: SyncScanOptions | undefined): string {\n const filterPrefix = literalPrefixForSyncFilters(filters)\n if (filterPrefix === '') return this.rawPrefix\n if (this.rawPrefix !== '' && !this.rawPrefix.endsWith('/')) return this.rawPrefix\n return `${this.rawPrefix}${rawPrefixBeforeNormalizedSeparator(filterPrefix)}`\n }\n\n private visibleCandidates(\n grouped: Map<string, B2ScanEntry>,\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const visible: VisibleB2ScanEntry[] = []\n\n for (const [fileName, entry] of grouped) {\n entry.versions.sort((a, b) => b.uploadTimestamp - a.uploadTimestamp)\n const selected = entry.versions[0]\n if (!selected || selected.action === FileAction.Hide) continue\n\n if (\n filters?.requireLocalSafePaths === true &&\n localFilesystemSyncPathIsUnsafe(entry.relativePath)\n ) {\n this.emitSkip(\n filters,\n entry.relativePath,\n fileName,\n 'local-unsafe-name',\n 'object name is unsafe for a local filesystem destination',\n )\n continue\n }\n\n visible.push({\n fileName,\n relativePath: entry.relativePath,\n versions: entry.versions,\n selectedVersion: selected,\n })\n }\n\n return visible\n }\n\n private rejectRelativePathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedRelativePaths = new Set<string>()\n\n for (const candidate of candidates) {\n if (collidedRelativePaths.has(candidate.relativePath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n 'object normalizes to a relative path already rejected because of another raw B2 key',\n )\n continue\n }\n\n const owner = owners.get(candidate.relativePath)\n if (owner !== undefined && owner.fileName !== candidate.fileName) {\n owners.delete(candidate.relativePath)\n removeAcceptedCandidate(accepted, owner)\n collidedRelativePaths.add(candidate.relativePath)\n this.emitSkip(\n filters,\n candidate.relativePath,\n owner.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(candidate.fileName)}`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(owner.fileName)}`,\n )\n continue\n }\n\n owners.set(candidate.relativePath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private rejectLocalPathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedLocalPaths = new Set<string>()\n\n for (const candidate of candidates) {\n const canonicalPath = localFilesystemCanonicalSyncPath(candidate.relativePath)\n if (collidedLocalPaths.has(canonicalPath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n 'object collides with another object on case-insensitive or Unicode-normalizing filesystems',\n )\n continue\n }\n\n const owner = owners.get(canonicalPath)\n if (owner !== undefined) {\n owners.delete(canonicalPath)\n removeAcceptedCandidate(accepted, owner)\n collidedLocalPaths.add(canonicalPath)\n this.emitSkip(\n filters,\n owner.relativePath,\n owner.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(candidate.fileName)} on local filesystems`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(owner.fileName)} on local filesystems`,\n )\n continue\n }\n\n owners.set(canonicalPath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private emitSkip(\n filters: SyncScanOptions | undefined,\n path: string,\n b2FileName: string,\n reason: SyncSkipReason,\n message: string,\n ): void {\n emitScannerSkip(filters, {\n type: 'skip',\n path,\n size: 0,\n message: `Skipped B2 object ${JSON.stringify(b2FileName)}: ${message}`,\n reason,\n b2FileName,\n })\n }\n}\n\nfunction rawPrefixBeforeNormalizedSeparator(filterPrefix: string): string {\n const separatorIndex = filterPrefix.indexOf('/')\n return separatorIndex === -1 ? filterPrefix : filterPrefix.slice(0, separatorIndex)\n}\n\nfunction emitScanError(options: SyncScanOptions, message: string, err: unknown): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path: '',\n size: 0,\n message: `${message}: ${sanitizeErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n}\n\nfunction removeAcceptedCandidate(\n candidates: VisibleB2ScanEntry[],\n target: VisibleB2ScanEntry,\n): void {\n const index = candidates.indexOf(target)\n if (index !== -1) candidates.splice(index, 1)\n}\n\nfunction scanIsAborted(filters: SyncScanOptions | undefined): boolean {\n return filters?.signal?.aborted === true\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"b2.js","names":[],"sources":["../../../src/sync/scanners/b2.ts"],"sourcesContent":["import type { Bucket } from '../../bucket.ts'\nimport {\n FileAction,\n type FileVersion,\n type ListedConcreteFileVersion,\n type ListFileVersionsResponse,\n} from '../../types/file.ts'\nimport type { FileId } from '../../types/ids.ts'\nimport { isSignalAbortError } from '../../util/abort.ts'\nimport { sanitizeErrorReason } from '../../util/error-reason.ts'\nimport {\n literalPrefixForSyncFilters,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { compareCodeUnits, compareSyncRelativePaths } from '../path-order.ts'\nimport { assertSyncPathAllowed } from '../paths.ts'\nimport {\n asRawB2KeyPrefix,\n b2KeyToRelativePathUnderPrefix,\n localFilesystemCanonicalSyncPath,\n localFilesystemSyncPathIsUnsafe,\n} from '../prefix.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport { selectB2ComparableSha1, syncSha1StateOf } from '../sha1-metadata.ts'\nimport type {\n B2SyncPath,\n SyncErrorEvent,\n SyncFolder,\n SyncScanOptions,\n SyncSkipReason,\n} from '../types.ts'\n\nconst MAX_EMPTY_B2_SCAN_PAGES = 100\n\ninterface B2ScanEntry {\n relativePath: string\n versions: ListedConcreteFileVersion[]\n}\n\ninterface VisibleB2ScanEntry extends B2ScanEntry {\n fileName: string\n selectedVersion: FileVersion\n}\n\n/**\n * Scans a B2 bucket (optionally filtered by a raw B2 key prefix) and yields\n * {@link B2SyncPath} entries sorted by `compareSyncRelativePaths(relativePath)`. Hidden files are excluded.\n * Raw B2 file names are used only as an internal tie-breaker after collision handling.\n * All versions for the listed prefix are fetched, grouped, and sorted before\n * yielding; exclude filters are applied client-side and do not reduce that\n * B2 listing memory footprint.\n * SDK-reserved temporary names fail the scan because syncing them could corrupt\n * in-progress transfers.\n */\nexport class B2Folder implements SyncFolder {\n readonly type = 'b2' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Raw B2 key prefix this folder scans, preserving caller-provided separators verbatim. */\n readonly rawPrefix: string\n private readonly bucket: Bucket\n\n /**\n * Creates a new B2Folder for the given bucket and optional prefix.\n * @param bucket - The B2 bucket to scan.\n * @param prefix - Optional raw B2 key prefix to restrict the scan scope.\n * Backslashes are preserved as raw B2 key characters; pass `/` explicitly for slash prefixes.\n */\n constructor(bucket: Bucket, prefix = '') {\n this.bucket = bucket\n this.rawPrefix = asRawB2KeyPrefix(prefix)\n }\n\n /**\n * Lists all file versions in the bucket, groups by name, and yields the latest visible version.\n * @param options - Optional scan controls.\n */\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pagination, grouping, and filtering stay in one async iterator.\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<B2SyncPath> {\n validateSyncFilters(options)\n const maxScanEntries = scanEntryLimit(options)\n const grouped = new Map<string, B2ScanEntry>()\n const listPrefix = this.listPrefixFor(options)\n\n let listedVersions = 0\n let startFileName: string | undefined\n let startFileId: FileId | undefined\n let emptyPageCount = 0\n\n while (true) {\n if (scanIsAborted(options)) return\n\n let listing: ListFileVersionsResponse\n try {\n listing = await this.bucket.listFileVersions({\n ...(listPrefix !== '' ? { prefix: listPrefix } : {}),\n ...(startFileName !== undefined ? { startFileName } : {}),\n ...(startFileId !== undefined ? { startFileId } : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n })\n } catch (err) {\n if (isSignalAbortError(options.signal, err)) return\n throw emitScanError(options, 'failed to scan B2 file versions', err)\n }\n if (scanIsAborted(options)) return\n\n if (listing.files.length === 0) {\n emptyPageCount++\n if (emptyPageCount > MAX_EMPTY_B2_SCAN_PAGES) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination returned too many empty pages'),\n )\n }\n } else {\n emptyPageCount = 0\n }\n\n for (const fv of listing.files) {\n if (scanIsAborted(options)) return\n assertScanEntryLimit(listedVersions + 1, maxScanEntries)\n listedVersions++\n\n const runtimeFile = fv as { readonly action?: unknown; readonly fileId?: unknown }\n if (runtimeFile.action === FileAction.Folder || typeof runtimeFile.fileId !== 'string') {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error(\n 'B2 listFileVersions returned a delimiter folder row or row without a string fileId even though the scanner did not request delimiter grouping',\n ),\n )\n }\n\n // Real B2 honors the prefix in listFileVersions, but custom\n // transports and the simulator can over-return. Guard before\n // stripping the raw prefix so relativePath is never corrupted.\n if (this.rawPrefix !== '' && !fv.fileName.startsWith(this.rawPrefix)) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'outside-prefix',\n `listed object is outside configured B2 prefix ${JSON.stringify(this.rawPrefix)}`,\n )\n continue\n }\n\n if (options.requireLocalSafePaths === true && fv.fileName.includes('\\\\')) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'local-unsafe-name',\n 'object name contains a backslash that is unsafe for local filesystem destinations',\n )\n continue\n }\n\n const relativePath = this.tryToRelativePath(fv.fileName)\n if (relativePath === null) {\n this.emitSkip(\n options,\n fv.fileName,\n fv.fileName,\n 'unsafe-name',\n 'object name cannot be represented as a safe sync relative path',\n )\n continue\n }\n\n if (!pathPassesSyncFilters(relativePath, options)) {\n if (pathSkippedByRegExpInputLimit(relativePath, options)) {\n emitScannerSkip(options, {\n ...regexpInputTooLongSkip(relativePath),\n b2FileName: fv.fileName,\n })\n }\n continue\n }\n\n const existing = grouped.get(fv.fileName)\n if (existing) {\n existing.versions.push(fv)\n } else {\n grouped.set(fv.fileName, { relativePath, versions: [fv] })\n }\n }\n\n if (!listing.nextFileName) break\n if (\n listing.nextFileName === startFileName &&\n (listing.nextFileId ?? undefined) === startFileId\n ) {\n throw emitScanError(\n options,\n 'failed to scan B2 file versions',\n new Error('B2 pagination did not advance'),\n )\n }\n startFileName = listing.nextFileName\n startFileId = listing.nextFileId ?? undefined\n }\n\n const visible = this.visibleCandidates(grouped, options)\n const withoutRelativeCollisions = this.rejectRelativePathCollisions(visible, options)\n const safeCandidates =\n options.requireLocalSafePaths === true\n ? this.rejectLocalPathCollisions(withoutRelativeCollisions, options)\n : withoutRelativeCollisions\n const sorted = safeCandidates.sort(\n (a, b) =>\n compareSyncRelativePaths(a.relativePath, b.relativePath) ||\n compareCodeUnits(a.fileName, b.fileName),\n )\n\n for (const { relativePath, versions, selectedVersion } of sorted) {\n if (scanIsAborted(options)) return\n // Reserved SDK temp names are a hard safety failure, not an ordinary filter.\n assertSyncPathAllowed(relativePath)\n const contentSha1 = selectB2ComparableSha1(selectedVersion)\n yield {\n relativePath,\n modTimeMillis: selectedVersion.uploadTimestamp,\n size: selectedVersion.contentLength,\n contentSha1,\n contentSha1State: syncSha1StateOf({ contentSha1 }),\n selectedVersion,\n allVersions: versions,\n }\n }\n }\n\n private tryToRelativePath(fileName: string): string | null {\n try {\n return b2KeyToRelativePathUnderPrefix(this.rawPrefix, fileName)\n } catch {\n return null\n }\n }\n\n private listPrefixFor(filters: SyncScanOptions | undefined): string {\n const filterPrefix = literalPrefixForSyncFilters(filters)\n if (filterPrefix === '') return this.rawPrefix\n if (this.rawPrefix !== '' && !this.rawPrefix.endsWith('/')) return this.rawPrefix\n return `${this.rawPrefix}${rawPrefixBeforeNormalizedSeparator(filterPrefix)}`\n }\n\n private visibleCandidates(\n grouped: Map<string, B2ScanEntry>,\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const visible: VisibleB2ScanEntry[] = []\n\n for (const [fileName, entry] of grouped) {\n entry.versions.sort((a, b) => b.uploadTimestamp - a.uploadTimestamp)\n const selected = entry.versions[0]\n if (!selected || selected.action === FileAction.Hide) continue\n\n if (\n filters?.requireLocalSafePaths === true &&\n localFilesystemSyncPathIsUnsafe(entry.relativePath)\n ) {\n this.emitSkip(\n filters,\n entry.relativePath,\n fileName,\n 'local-unsafe-name',\n 'object name is unsafe for a local filesystem destination',\n )\n continue\n }\n\n visible.push({\n fileName,\n relativePath: entry.relativePath,\n versions: entry.versions,\n selectedVersion: selected,\n })\n }\n\n return visible\n }\n\n private rejectRelativePathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedRelativePaths = new Set<string>()\n\n for (const candidate of candidates) {\n if (collidedRelativePaths.has(candidate.relativePath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n 'object normalizes to a relative path already rejected because of another raw B2 key',\n )\n continue\n }\n\n const owner = owners.get(candidate.relativePath)\n if (owner !== undefined && owner.fileName !== candidate.fileName) {\n owners.delete(candidate.relativePath)\n removeAcceptedCandidate(accepted, owner)\n collidedRelativePaths.add(candidate.relativePath)\n this.emitSkip(\n filters,\n candidate.relativePath,\n owner.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(candidate.fileName)}`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'relative-path-collision',\n `object normalizes to the same relative path as ${JSON.stringify(owner.fileName)}`,\n )\n continue\n }\n\n owners.set(candidate.relativePath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private rejectLocalPathCollisions(\n candidates: VisibleB2ScanEntry[],\n filters: SyncScanOptions | undefined,\n ): VisibleB2ScanEntry[] {\n const accepted: VisibleB2ScanEntry[] = []\n const owners = new Map<string, VisibleB2ScanEntry>()\n const collidedLocalPaths = new Set<string>()\n\n for (const candidate of candidates) {\n const canonicalPath = localFilesystemCanonicalSyncPath(candidate.relativePath)\n if (collidedLocalPaths.has(canonicalPath)) {\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n 'object collides with another object on case-insensitive or Unicode-normalizing filesystems',\n )\n continue\n }\n\n const owner = owners.get(canonicalPath)\n if (owner !== undefined) {\n owners.delete(canonicalPath)\n removeAcceptedCandidate(accepted, owner)\n collidedLocalPaths.add(canonicalPath)\n this.emitSkip(\n filters,\n owner.relativePath,\n owner.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(candidate.fileName)} on local filesystems`,\n )\n this.emitSkip(\n filters,\n candidate.relativePath,\n candidate.fileName,\n 'local-path-collision',\n `object collides with ${JSON.stringify(owner.fileName)} on local filesystems`,\n )\n continue\n }\n\n owners.set(canonicalPath, candidate)\n accepted.push(candidate)\n }\n\n return accepted\n }\n\n private emitSkip(\n filters: SyncScanOptions | undefined,\n path: string,\n b2FileName: string,\n reason: SyncSkipReason,\n message: string,\n ): void {\n emitScannerSkip(filters, {\n type: 'skip',\n path,\n size: 0,\n message: `Skipped B2 object ${JSON.stringify(b2FileName)}: ${message}`,\n reason,\n b2FileName,\n })\n }\n}\n\nfunction rawPrefixBeforeNormalizedSeparator(filterPrefix: string): string {\n const separatorIndex = filterPrefix.indexOf('/')\n return separatorIndex === -1 ? filterPrefix : filterPrefix.slice(0, separatorIndex)\n}\n\nfunction emitScanError(options: SyncScanOptions, message: string, err: unknown): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path: '',\n size: 0,\n message: `${message}: ${sanitizeErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n}\n\nfunction removeAcceptedCandidate(\n candidates: VisibleB2ScanEntry[],\n target: VisibleB2ScanEntry,\n): void {\n const index = candidates.indexOf(target)\n if (index !== -1) candidates.splice(index, 1)\n}\n\nfunction scanIsAborted(filters: SyncScanOptions | undefined): boolean {\n return filters?.signal?.aborted === true\n}\n"],"mappings":"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"}
@@ -4,9 +4,9 @@ const require_filters = require("../filters.cjs");
4
4
  const require_path_order = require("../path-order.cjs");
5
5
  const require_scan_limit = require("../scan-limit.cjs");
6
6
  const require_local_file_identity = require("../local-file-identity.cjs");
7
+ const require_filesystem_errors = require("../filesystem-errors.cjs");
7
8
  const require_path_safety = require("../path-safety.cjs");
8
9
  const require_download_staging = require("../download-staging.cjs");
9
- const require_filesystem_errors = require("../filesystem-errors.cjs");
10
10
  const require_local_filesystem_root = require("../local-filesystem-root.cjs");
11
11
  //#region src/sync/scanners/local.ts
12
12
  /**
@@ -40,8 +40,17 @@ var LocalFolder = class {
40
40
  require_regexp_safety.validateSyncFilters(options);
41
41
  const nodeDeps = await loadLocalNodeDeps();
42
42
  const root = nodeDeps.resolve(this.root);
43
+ const followSymlinks = options.localSymlinks === "follow" && options.requireLocalSafePaths !== true;
44
+ let scanRoot = root;
43
45
  const collected = [];
44
- await this.walk(root, root, collected, options, require_scan_limit.scanEntryLimit(options), nodeDeps);
46
+ const activeDirectoryIds = /* @__PURE__ */ new Set();
47
+ const followedDirectoryIds = /* @__PURE__ */ new Set();
48
+ if (followSymlinks) try {
49
+ scanRoot = await nodeDeps.realpath(root);
50
+ const rootStats = await nodeDeps.stat(scanRoot);
51
+ if (rootStats.isDirectory()) activeDirectoryIds.add(localNodeIdentity(rootStats));
52
+ } catch {}
53
+ await this.walk(scanRoot, scanRoot, "", collected, options, require_scan_limit.scanEntryLimit(options), nodeDeps, activeDirectoryIds, followedDirectoryIds);
45
54
  collected.sort((a, b) => require_path_order.compareSyncRelativePaths(a.relativePath, b.relativePath));
46
55
  for (const entry of collected) {
47
56
  throwIfScanAborted(options);
@@ -52,25 +61,28 @@ var LocalFolder = class {
52
61
  * Recursively collects files from {@link dir} into {@link out}.
53
62
  * @param root - Resolved scan root used for relative path calculation.
54
63
  * @param dir - Absolute path of the directory to scan.
64
+ * @param dirRel - Sync-relative path represented by {@link dir}.
55
65
  * @param out - Accumulator array that receives discovered file entries.
56
66
  * @param options - Optional scan controls.
57
67
  * @param maxScanEntries - Maximum number of entries to retain before failing.
58
68
  * @param nodeDeps - Lazy-loaded Node filesystem and path helpers.
69
+ * @param activeDirectoryIds - Directory identities in the current recursion stack.
70
+ * @param followedDirectoryIds - Followed symlink directory identities already traversed.
59
71
  */
60
- async walk(root, dir, out, options, maxScanEntries, nodeDeps) {
72
+ async walk(root, dir, dirRel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds) {
61
73
  throwIfScanAborted(options);
62
74
  let entries;
63
75
  try {
64
76
  entries = await nodeDeps.readdir(dir, { withFileTypes: true });
65
77
  } catch (err) {
66
- const error = this.emitScanError(options, relativePathFromRoot(root, dir, nodeDeps), "directory", err);
78
+ const error = this.emitScanError(options, dirRel, "directory", err);
67
79
  if (dir === root) throw error;
68
80
  return;
69
81
  }
70
82
  for (const entry of entries) {
71
83
  throwIfScanAborted(options);
72
84
  const fullPath = nodeDeps.join(dir, entry.name);
73
- const rel = relativePathFromRoot(root, fullPath, nodeDeps);
85
+ const rel = joinSyncRelativePath(dirRel, entry.name);
74
86
  if (require_download_staging.isDownloadStagingDirectorySegment(rel) && entry.isDirectory() && require_download_staging.isDownloadStagingDirectorySegment(entry.name) && await require_download_staging.isManagedDownloadStagingRoot(fullPath)) continue;
75
87
  if (rel.includes("\\")) {
76
88
  require_scan_events.emitScannerSkip(options, {
@@ -93,35 +105,130 @@ var LocalFolder = class {
93
105
  continue;
94
106
  }
95
107
  if (entry.isDirectory()) {
96
- if (require_filters.directoryMayContainSyncPaths(rel, options)) await this.walk(root, fullPath, out, options, maxScanEntries, nodeDeps);
97
- } else if (entry.isFile()) {
98
- if (!require_filters.pathPassesSyncFilters(rel, options)) {
99
- if (require_filters.pathSkippedByRegExpInputLimit(rel, options)) require_scan_events.emitScannerSkip(options, require_scan_events.regexpInputTooLongSkip(rel));
100
- continue;
101
- }
102
- let s;
103
- try {
104
- s = await nodeDeps.lstat(fullPath);
105
- /* v8 ignore start -- lstat race after a Dirent file result is not deterministic */
106
- if (!s.isFile()) {
107
- this.emitScanError(options, rel, "file", /* @__PURE__ */ new Error("not a regular file"));
108
- continue;
109
- }
110
- } catch (err) {
111
- /* v8 ignore next -- stat TOCTOU failures are not deterministic to trigger */
112
- this.emitScanError(options, relativePathFromRoot(root, fullPath, nodeDeps), "file", err);
113
- continue;
108
+ if (require_filters.directoryMayContainSyncPaths(rel, options)) await this.walkDirectory(root, fullPath, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds);
109
+ } else if (entry.isFile()) await this.collectFile(fullPath, rel, out, options, maxScanEntries, nodeDeps);
110
+ else if (entry.isSymbolicLink()) await this.handleSymlink(root, fullPath, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds);
111
+ }
112
+ }
113
+ async walkDirectory(root, dir, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds, knownStats) {
114
+ let stats = knownStats;
115
+ if (stats === void 0) try {
116
+ stats = await nodeDeps.stat(dir);
117
+ } catch (err) {
118
+ /* v8 ignore start -- stat TOCTOU failures after a Dirent directory result are not deterministic */
119
+ this.emitScanError(options, rel, "directory", err);
120
+ return;
121
+ }
122
+ /* v8 ignore start -- stat TOCTOU races after a Dirent directory result are not deterministic */
123
+ if (!stats.isDirectory()) {
124
+ this.emitScanError(options, rel, "directory", /* @__PURE__ */ new Error("not a directory"));
125
+ return;
126
+ }
127
+ /* v8 ignore stop */
128
+ const identity = localNodeIdentity(stats);
129
+ if (activeDirectoryIds.has(identity)) {
130
+ require_scan_events.emitScannerSkip(options, {
131
+ type: "skip",
132
+ path: rel,
133
+ size: 0,
134
+ reason: "local-symlink-loop",
135
+ message: `Skipped local path ${JSON.stringify(rel)}: symlink would revisit an ancestor directory`
136
+ });
137
+ return;
138
+ }
139
+ activeDirectoryIds.add(identity);
140
+ try {
141
+ await this.walk(root, dir, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds);
142
+ } finally {
143
+ activeDirectoryIds.delete(identity);
144
+ }
145
+ }
146
+ async collectFile(fullPath, rel, out, options, maxScanEntries, nodeDeps, knownStats) {
147
+ if (!require_filters.pathPassesSyncFilters(rel, options)) {
148
+ if (require_filters.pathSkippedByRegExpInputLimit(rel, options)) require_scan_events.emitScannerSkip(options, require_scan_events.regexpInputTooLongSkip(rel));
149
+ return;
150
+ }
151
+ let s = knownStats;
152
+ if (s === void 0) try {
153
+ s = await nodeDeps.lstat(fullPath);
154
+ /* v8 ignore start -- lstat race after a Dirent file result is not deterministic */
155
+ if (!s.isFile()) {
156
+ this.emitScanError(options, rel, "file", /* @__PURE__ */ new Error("not a regular file"));
157
+ return;
158
+ }
159
+ } catch (err) {
160
+ /* v8 ignore start -- stat TOCTOU failures are not deterministic to trigger */
161
+ this.emitScanError(options, rel, "file", err);
162
+ return;
163
+ }
164
+ require_scan_limit.assertScanEntryLimit(out.length + 1, maxScanEntries);
165
+ out.push({
166
+ relativePath: rel,
167
+ absolutePath: fullPath,
168
+ modTimeMillis: Math.floor(s.mtimeMs),
169
+ size: s.size,
170
+ fileIdentity: require_local_file_identity.localFileIdentityFromStats(s)
171
+ });
172
+ }
173
+ async handleSymlink(root, fullPath, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds) {
174
+ if ((options.localSymlinks ?? "skip") === "skip" || options.requireLocalSafePaths === true) {
175
+ require_scan_events.emitScannerSkip(options, {
176
+ type: "skip",
177
+ path: rel,
178
+ size: 0,
179
+ reason: "local-symlink",
180
+ message: `Skipped local path ${JSON.stringify(rel)}: local symlinks are disabled`
181
+ });
182
+ return;
183
+ }
184
+ let targetPath;
185
+ let targetStats;
186
+ try {
187
+ targetPath = await nodeDeps.realpath(fullPath);
188
+ targetStats = await nodeDeps.stat(targetPath);
189
+ } catch (err) {
190
+ this.emitScanError(options, rel, "symlink", err);
191
+ return;
192
+ }
193
+ if (!isPathInsideRoot(root, targetPath, nodeDeps)) {
194
+ require_scan_events.emitScannerSkip(options, {
195
+ type: "skip",
196
+ path: rel,
197
+ size: 0,
198
+ reason: "local-symlink",
199
+ message: `Skipped local path ${JSON.stringify(rel)}: symlink target is outside the sync root`
200
+ });
201
+ return;
202
+ }
203
+ if (targetStats.isDirectory()) {
204
+ if (require_filters.directoryMayContainSyncPaths(rel, options)) {
205
+ const identity = localNodeIdentity(targetStats);
206
+ if (followedDirectoryIds.has(identity)) {
207
+ require_scan_events.emitScannerSkip(options, {
208
+ type: "skip",
209
+ path: rel,
210
+ size: 0,
211
+ reason: "local-symlink-loop",
212
+ message: `Skipped local path ${JSON.stringify(rel)}: symlink target was already visited`
213
+ });
214
+ return;
114
215
  }
115
- require_scan_limit.assertScanEntryLimit(out.length + 1, maxScanEntries);
116
- out.push({
117
- relativePath: rel,
118
- absolutePath: fullPath,
119
- modTimeMillis: Math.floor(s.mtimeMs),
120
- size: s.size,
121
- fileIdentity: require_local_file_identity.localFileIdentityFromStats(s)
122
- });
216
+ followedDirectoryIds.add(identity);
217
+ await this.walkDirectory(root, targetPath, rel, out, options, maxScanEntries, nodeDeps, activeDirectoryIds, followedDirectoryIds, targetStats);
123
218
  }
219
+ return;
124
220
  }
221
+ if (targetStats.isFile()) {
222
+ await this.collectFile(targetPath, rel, out, options, maxScanEntries, nodeDeps, targetStats);
223
+ return;
224
+ }
225
+ require_scan_events.emitScannerSkip(options, {
226
+ type: "skip",
227
+ path: rel,
228
+ size: 0,
229
+ reason: "local-symlink",
230
+ message: `Skipped local path ${JSON.stringify(rel)}: symlink target is not a regular file or directory`
231
+ });
125
232
  }
126
233
  emitScanError(options, path, kind, err) {
127
234
  const event = {
@@ -139,9 +246,12 @@ async function loadLocalNodeDeps() {
139
246
  return {
140
247
  readdir: fsPromises.readdir,
141
248
  lstat: fsPromises.lstat,
249
+ stat: fsPromises.stat,
142
250
  join: path.join,
143
251
  relative: path.relative,
252
+ realpath: fsPromises.realpath,
144
253
  resolve: path.resolve,
254
+ isAbsolute: path.isAbsolute,
145
255
  sep: path.sep
146
256
  };
147
257
  }
@@ -205,8 +315,15 @@ function normalizePathSegments(segments, separator) {
205
315
  }
206
316
  return out.join(separator);
207
317
  }
208
- function relativePathFromRoot(root, path, nodeDeps) {
209
- return nodeDeps.relative(root, path).split(nodeDeps.sep).join("/");
318
+ function joinSyncRelativePath(parent, child) {
319
+ return parent === "" ? child : `${parent}/${child}`;
320
+ }
321
+ function isPathInsideRoot(root, path, nodeDeps) {
322
+ const rel = nodeDeps.relative(root, path);
323
+ return rel === "" || !rel.startsWith(`..${nodeDeps.sep}`) && rel !== ".." && !nodeDeps.isAbsolute(rel);
324
+ }
325
+ function localNodeIdentity(stats) {
326
+ return `${stats.dev}:${stats.ino}`;
210
327
  }
211
328
  function throwIfScanAborted(options) {
212
329
  if (options.signal?.aborted === true) throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
@@ -1 +1 @@
1
- {"version":3,"file":"local.cjs","names":[],"sources":["../../../src/sync/scanners/local.ts"],"sourcesContent":["import {\n isDownloadStagingDirectorySegment,\n isManagedDownloadStagingRoot,\n} from '../download-staging.ts'\nimport { localFilesystemErrorReason } from '../filesystem-errors.ts'\nimport {\n directoryMayContainSyncPaths,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { localFileIdentityFromStats } from '../local-file-identity.ts'\nimport { registerLocalFilesystemRoot } from '../local-filesystem-root.ts'\nimport { compareSyncRelativePaths } from '../path-order.ts'\nimport { isReservedSyncTempFileName } from '../path-safety.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport type { LocalSyncPath, SyncErrorEvent, SyncFolder, SyncScanOptions } from '../types.ts'\n\ntype LocalDirent = {\n readonly name: string\n isDirectory(): boolean\n isFile(): boolean\n}\n\ntype LocalStats = {\n readonly dev: number\n readonly ino: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n readonly size: number\n isFile(): boolean\n}\n\ntype LocalNodeDeps = {\n readdir(path: string, options: { readonly withFileTypes: true }): Promise<LocalDirent[]>\n lstat(path: string): Promise<LocalStats>\n join(...paths: string[]): string\n relative(from: string, to: string): string\n resolve(...paths: string[]): string\n sep: string\n}\n\n/**\n * Scans a local directory tree and yields {@link LocalSyncPath} entries sorted by relative path.\n * A root directory read failure aborts the scan with an error diagnostic. Per-entry file or\n * directory failures are reported through `onError` and the scan continues over readable siblings.\n * SDK-managed partial download file names are skipped so unfinished internal\n * temp files are not synchronized.\n * The current implementation collects matching entries before sorting, so memory usage is\n * proportional to the number of matched files.\n */\nexport class LocalFolder implements SyncFolder {\n readonly type = 'local' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Resolved absolute path to the local root directory. */\n readonly root: string\n\n /**\n * Creates a new LocalFolder for the given root directory.\n * @param root - Absolute or relative path to the local directory to scan.\n */\n constructor(root: string) {\n this.root = resolvePathAtConstruction(root)\n registerLocalFilesystemRoot(this)\n }\n\n /**\n * Recursively walks the directory and yields files in sync path order.\n * @param options - Optional scan controls.\n */\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<LocalSyncPath> {\n validateSyncFilters(options)\n const nodeDeps = await loadLocalNodeDeps()\n const root = nodeDeps.resolve(this.root)\n const collected: LocalSyncPath[] = []\n await this.walk(root, root, collected, options, scanEntryLimit(options), nodeDeps)\n collected.sort((a, b) => compareSyncRelativePaths(a.relativePath, b.relativePath))\n for (const entry of collected) {\n throwIfScanAborted(options)\n yield entry\n }\n }\n\n /**\n * Recursively collects files from {@link dir} into {@link out}.\n * @param root - Resolved scan root used for relative path calculation.\n * @param dir - Absolute path of the directory to scan.\n * @param out - Accumulator array that receives discovered file entries.\n * @param options - Optional scan controls.\n * @param maxScanEntries - Maximum number of entries to retain before failing.\n * @param nodeDeps - Lazy-loaded Node filesystem and path helpers.\n */\n private async walk(\n root: string,\n dir: string,\n out: LocalSyncPath[],\n options: SyncScanOptions,\n maxScanEntries: number,\n nodeDeps: LocalNodeDeps,\n ): Promise<void> {\n throwIfScanAborted(options)\n let entries: LocalDirent[]\n try {\n entries = await nodeDeps.readdir(dir, { withFileTypes: true })\n } catch (err) {\n const error = this.emitScanError(\n options,\n relativePathFromRoot(root, dir, nodeDeps),\n 'directory',\n err,\n )\n if (dir === root) throw error\n return\n }\n\n for (const entry of entries) {\n throwIfScanAborted(options)\n const fullPath = nodeDeps.join(dir, entry.name)\n const rel = relativePathFromRoot(root, fullPath, nodeDeps)\n if (\n isDownloadStagingDirectorySegment(rel) &&\n entry.isDirectory() &&\n isDownloadStagingDirectorySegment(entry.name) &&\n (await isManagedDownloadStagingRoot(fullPath))\n ) {\n continue\n }\n if (rel.includes('\\\\')) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'unsafe-name',\n message: `Skipped local path ${JSON.stringify(rel)}: backslashes are not safe sync path characters`,\n })\n continue\n }\n if (isReservedSyncTempFileName(entry.name)) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'stale-download-partial',\n message: `Skipped local path ${JSON.stringify(rel)}: reserved SDK partial download file`,\n })\n continue\n }\n // Symlinks, FIFOs, sockets, and device nodes are not syncable files.\n // Ignore them without poisoning delete-mode orphan handling for unrelated paths.\n if (entry.isDirectory()) {\n if (directoryMayContainSyncPaths(rel, options)) {\n await this.walk(root, fullPath, out, options, maxScanEntries, nodeDeps)\n }\n } else if (entry.isFile()) {\n if (!pathPassesSyncFilters(rel, options)) {\n if (pathSkippedByRegExpInputLimit(rel, options)) {\n emitScannerSkip(options, regexpInputTooLongSkip(rel))\n }\n continue\n }\n let s: LocalStats\n try {\n s = await nodeDeps.lstat(fullPath)\n /* v8 ignore start -- lstat race after a Dirent file result is not deterministic */\n if (!s.isFile()) {\n this.emitScanError(options, rel, 'file', new Error('not a regular file'))\n continue\n }\n /* v8 ignore stop */\n } catch (err) {\n /* v8 ignore next -- stat TOCTOU failures are not deterministic to trigger */\n this.emitScanError(options, relativePathFromRoot(root, fullPath, nodeDeps), 'file', err)\n continue\n }\n assertScanEntryLimit(out.length + 1, maxScanEntries)\n out.push({\n relativePath: rel,\n absolutePath: fullPath,\n modTimeMillis: Math.floor(s.mtimeMs),\n size: s.size,\n fileIdentity: localFileIdentityFromStats(s),\n })\n }\n }\n }\n\n private emitScanError(\n options: SyncScanOptions,\n path: string,\n kind: 'directory' | 'file',\n err: unknown,\n ): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path,\n size: 0,\n message: `failed to scan local ${kind}: ${localFilesystemErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n }\n}\n\nasync function loadLocalNodeDeps(): Promise<LocalNodeDeps> {\n const [fsPromises, path] = await Promise.all([import('node:fs/promises'), import('node:path')])\n return {\n readdir: fsPromises.readdir as LocalNodeDeps['readdir'],\n lstat: fsPromises.lstat as LocalNodeDeps['lstat'],\n join: path.join,\n relative: path.relative,\n resolve: path.resolve,\n sep: path.sep,\n }\n}\n\nfunction resolvePathAtConstruction(root: string): string {\n const processLike = (\n globalThis as {\n process?: { cwd?: () => string; platform?: string }\n }\n ).process\n if (typeof processLike?.cwd !== 'function') return root\n\n const cwd = processLike.cwd()\n if (processLike.platform === 'win32') return resolveWindowsPath(cwd, root)\n return resolvePosixPath(cwd, root)\n}\n\nfunction resolvePosixPath(cwd: string, root: string): string {\n const joined = root.startsWith('/') ? root : `${cwd}/${root}`\n const resolved = normalizePathSegments(joined.split('/'), '/')\n return resolved === '' ? '/' : `/${resolved}`\n}\n\nfunction resolveWindowsPath(cwd: string, root: string): string {\n const normalizedRoot = root.replaceAll('/', '\\\\')\n const normalizedCwd = cwd.replaceAll('/', '\\\\')\n const drive = /^[A-Za-z]:/.exec(normalizedCwd)?.[0] ?? ''\n const cwdUnc = splitUncPath(normalizedCwd)\n if (/^\\\\\\\\/.test(normalizedRoot)) return normalizeUncPath(normalizedRoot)\n if (/^[A-Za-z]:\\\\/.test(normalizedRoot)) {\n const prefix = normalizedRoot.slice(0, 2)\n const rest = normalizedRoot.slice(3).split('\\\\')\n return joinWindowsRoot(prefix, normalizePathSegments(rest, '\\\\'))\n }\n if (/^[A-Za-z]:/.test(normalizedRoot)) {\n throw new Error('LocalFolder root must not be a drive-relative Windows path')\n }\n if (normalizedRoot.startsWith('\\\\')) {\n const rest = normalizePathSegments(normalizedRoot.slice(1).split('\\\\'), '\\\\')\n return joinWindowsRoot(cwdUnc?.prefix ?? drive, rest)\n }\n if (cwdUnc !== undefined) {\n const resolved = normalizePathSegments([...cwdUnc.rest, ...normalizedRoot.split('\\\\')], '\\\\')\n return joinWindowsRoot(cwdUnc.prefix, resolved)\n }\n const base = /^[A-Za-z]:\\\\/.test(normalizedCwd) ? normalizedCwd : `${drive}\\\\`\n const prefix = /^[A-Za-z]:/.exec(base)?.[0] ?? drive\n const baseRest = base.slice(prefix.length).replace(/^\\\\/, '').split('\\\\')\n const resolved = normalizePathSegments([...baseRest, ...normalizedRoot.split('\\\\')], '\\\\')\n return joinWindowsRoot(prefix, resolved)\n}\n\nfunction normalizeUncPath(path: string): string {\n const unc = splitUncPath(path)\n if (unc === undefined) return path\n return joinWindowsRoot(unc.prefix, normalizePathSegments(unc.rest, '\\\\'))\n}\n\nfunction splitUncPath(\n path: string,\n): { readonly prefix: string; readonly rest: string[] } | undefined {\n if (!path.startsWith('\\\\\\\\')) return undefined\n const parts = path.split('\\\\').filter((part) => part !== '')\n const [server, share, ...rest] = parts\n if (server === undefined || share === undefined) return undefined\n return { prefix: `\\\\\\\\${server}\\\\${share}`, rest }\n}\n\nfunction joinWindowsRoot(prefix: string, rest: string): string {\n return rest === '' ? `${prefix}\\\\` : `${prefix}\\\\${rest}`\n}\n\nfunction normalizePathSegments(segments: readonly string[], separator: '/' | '\\\\'): string {\n const out: string[] = []\n for (const segment of segments) {\n if (segment === '' || segment === '.') continue\n if (segment === '..') {\n out.pop()\n continue\n }\n out.push(segment)\n }\n return out.join(separator)\n}\n\nfunction relativePathFromRoot(root: string, path: string, nodeDeps: LocalNodeDeps): string {\n return nodeDeps.relative(root, path).split(nodeDeps.sep).join('/')\n}\n\nfunction throwIfScanAborted(options: SyncScanOptions): void {\n if (options.signal?.aborted === true) {\n throw options.signal.reason ?? new DOMException('Aborted', 'AbortError')\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"local.cjs","names":[],"sources":["../../../src/sync/scanners/local.ts"],"sourcesContent":["import {\n isDownloadStagingDirectorySegment,\n isManagedDownloadStagingRoot,\n} from '../download-staging.ts'\nimport { localFilesystemErrorReason } from '../filesystem-errors.ts'\nimport {\n directoryMayContainSyncPaths,\n pathPassesSyncFilters,\n pathSkippedByRegExpInputLimit,\n} from '../filters.ts'\nimport { localFileIdentityFromStats } from '../local-file-identity.ts'\nimport { registerLocalFilesystemRoot } from '../local-filesystem-root.ts'\nimport { compareSyncRelativePaths } from '../path-order.ts'\nimport { isReservedSyncTempFileName } from '../path-safety.ts'\nimport { validateSyncFilters } from '../regexp-safety.ts'\nimport { emitScannerSkip, regexpInputTooLongSkip } from '../scan-events.ts'\nimport { assertScanEntryLimit, scanEntryLimit } from '../scan-limit.ts'\nimport type { LocalSyncPath, SyncErrorEvent, SyncFolder, SyncScanOptions } from '../types.ts'\n\ntype LocalDirent = {\n readonly name: string\n isDirectory(): boolean\n isFile(): boolean\n isSymbolicLink(): boolean\n}\n\ntype LocalStats = {\n readonly dev: number\n readonly ino: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n readonly size: number\n isDirectory(): boolean\n isFile(): boolean\n}\n\ntype LocalNodeDeps = {\n readdir(path: string, options: { readonly withFileTypes: true }): Promise<LocalDirent[]>\n lstat(path: string): Promise<LocalStats>\n stat(path: string): Promise<LocalStats>\n join(...paths: string[]): string\n relative(from: string, to: string): string\n realpath(path: string): Promise<string>\n resolve(...paths: string[]): string\n isAbsolute(path: string): boolean\n sep: string\n}\n\n/**\n * Scans a local directory tree and yields {@link LocalSyncPath} entries sorted by relative path.\n * A root directory read failure aborts the scan with an error diagnostic. Per-entry file or\n * directory failures are reported through `onError` and the scan continues over readable siblings.\n * SDK-managed partial download file names are skipped so unfinished internal\n * temp files are not synchronized.\n * The current implementation collects matching entries before sorting, so memory usage is\n * proportional to the number of matched files.\n */\nexport class LocalFolder implements SyncFolder {\n readonly type = 'local' as const\n readonly appliesScanFilters = true as const\n readonly appliesScanSorting = true as const\n /** Resolved absolute path to the local root directory. */\n readonly root: string\n\n /**\n * Creates a new LocalFolder for the given root directory.\n * @param root - Absolute or relative path to the local directory to scan.\n */\n constructor(root: string) {\n this.root = resolvePathAtConstruction(root)\n registerLocalFilesystemRoot(this)\n }\n\n /**\n * Recursively walks the directory and yields files in sync path order.\n * @param options - Optional scan controls.\n */\n async *scan(options: SyncScanOptions = {}): AsyncGenerator<LocalSyncPath> {\n validateSyncFilters(options)\n const nodeDeps = await loadLocalNodeDeps()\n const root = nodeDeps.resolve(this.root)\n const followSymlinks =\n options.localSymlinks === 'follow' && options.requireLocalSafePaths !== true\n let scanRoot = root\n const collected: LocalSyncPath[] = []\n const activeDirectoryIds = new Set<string>()\n const followedDirectoryIds = new Set<string>()\n if (followSymlinks) {\n try {\n scanRoot = await nodeDeps.realpath(root)\n const rootStats = await nodeDeps.stat(scanRoot)\n if (rootStats.isDirectory()) activeDirectoryIds.add(localNodeIdentity(rootStats))\n } catch {\n // Root read errors are reported by walk(), preserving the existing diagnostic shape.\n }\n }\n await this.walk(\n scanRoot,\n scanRoot,\n '',\n collected,\n options,\n scanEntryLimit(options),\n nodeDeps,\n activeDirectoryIds,\n followedDirectoryIds,\n )\n collected.sort((a, b) => compareSyncRelativePaths(a.relativePath, b.relativePath))\n for (const entry of collected) {\n throwIfScanAborted(options)\n yield entry\n }\n }\n\n /**\n * Recursively collects files from {@link dir} into {@link out}.\n * @param root - Resolved scan root used for relative path calculation.\n * @param dir - Absolute path of the directory to scan.\n * @param dirRel - Sync-relative path represented by {@link dir}.\n * @param out - Accumulator array that receives discovered file entries.\n * @param options - Optional scan controls.\n * @param maxScanEntries - Maximum number of entries to retain before failing.\n * @param nodeDeps - Lazy-loaded Node filesystem and path helpers.\n * @param activeDirectoryIds - Directory identities in the current recursion stack.\n * @param followedDirectoryIds - Followed symlink directory identities already traversed.\n */\n private async walk(\n root: string,\n dir: string,\n dirRel: string,\n out: LocalSyncPath[],\n options: SyncScanOptions,\n maxScanEntries: number,\n nodeDeps: LocalNodeDeps,\n activeDirectoryIds: Set<string>,\n followedDirectoryIds: Set<string>,\n ): Promise<void> {\n throwIfScanAborted(options)\n let entries: LocalDirent[]\n try {\n entries = await nodeDeps.readdir(dir, { withFileTypes: true })\n } catch (err) {\n const error = this.emitScanError(options, dirRel, 'directory', err)\n if (dir === root) throw error\n return\n }\n\n for (const entry of entries) {\n throwIfScanAborted(options)\n const fullPath = nodeDeps.join(dir, entry.name)\n const rel = joinSyncRelativePath(dirRel, entry.name)\n if (\n isDownloadStagingDirectorySegment(rel) &&\n entry.isDirectory() &&\n isDownloadStagingDirectorySegment(entry.name) &&\n (await isManagedDownloadStagingRoot(fullPath))\n ) {\n continue\n }\n if (rel.includes('\\\\')) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'unsafe-name',\n message: `Skipped local path ${JSON.stringify(rel)}: backslashes are not safe sync path characters`,\n })\n continue\n }\n if (isReservedSyncTempFileName(entry.name)) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'stale-download-partial',\n message: `Skipped local path ${JSON.stringify(rel)}: reserved SDK partial download file`,\n })\n continue\n }\n // Symlinks, FIFOs, sockets, and device nodes are not syncable files.\n // Ignore them without poisoning delete-mode orphan handling for unrelated paths.\n if (entry.isDirectory()) {\n if (directoryMayContainSyncPaths(rel, options)) {\n await this.walkDirectory(\n root,\n fullPath,\n rel,\n out,\n options,\n maxScanEntries,\n nodeDeps,\n activeDirectoryIds,\n followedDirectoryIds,\n )\n }\n } else if (entry.isFile()) {\n await this.collectFile(fullPath, rel, out, options, maxScanEntries, nodeDeps)\n } else if (entry.isSymbolicLink()) {\n await this.handleSymlink(\n root,\n fullPath,\n rel,\n out,\n options,\n maxScanEntries,\n nodeDeps,\n activeDirectoryIds,\n followedDirectoryIds,\n )\n }\n }\n }\n\n private async walkDirectory(\n root: string,\n dir: string,\n rel: string,\n out: LocalSyncPath[],\n options: SyncScanOptions,\n maxScanEntries: number,\n nodeDeps: LocalNodeDeps,\n activeDirectoryIds: Set<string>,\n followedDirectoryIds: Set<string>,\n knownStats?: LocalStats,\n ): Promise<void> {\n let stats = knownStats\n if (stats === undefined) {\n try {\n stats = await nodeDeps.stat(dir)\n } catch (err) {\n /* v8 ignore start -- stat TOCTOU failures after a Dirent directory result are not deterministic */\n this.emitScanError(options, rel, 'directory', err)\n return\n /* v8 ignore stop */\n }\n }\n /* v8 ignore start -- stat TOCTOU races after a Dirent directory result are not deterministic */\n if (!stats.isDirectory()) {\n this.emitScanError(options, rel, 'directory', new Error('not a directory'))\n return\n }\n /* v8 ignore stop */\n\n const identity = localNodeIdentity(stats)\n if (activeDirectoryIds.has(identity)) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'local-symlink-loop',\n message: `Skipped local path ${JSON.stringify(rel)}: symlink would revisit an ancestor directory`,\n })\n return\n }\n\n activeDirectoryIds.add(identity)\n try {\n await this.walk(\n root,\n dir,\n rel,\n out,\n options,\n maxScanEntries,\n nodeDeps,\n activeDirectoryIds,\n followedDirectoryIds,\n )\n } finally {\n activeDirectoryIds.delete(identity)\n }\n }\n\n private async collectFile(\n fullPath: string,\n rel: string,\n out: LocalSyncPath[],\n options: SyncScanOptions,\n maxScanEntries: number,\n nodeDeps: LocalNodeDeps,\n knownStats?: LocalStats,\n ): Promise<void> {\n if (!pathPassesSyncFilters(rel, options)) {\n if (pathSkippedByRegExpInputLimit(rel, options)) {\n emitScannerSkip(options, regexpInputTooLongSkip(rel))\n }\n return\n }\n\n let s = knownStats\n if (s === undefined) {\n try {\n s = await nodeDeps.lstat(fullPath)\n /* v8 ignore start -- lstat race after a Dirent file result is not deterministic */\n if (!s.isFile()) {\n this.emitScanError(options, rel, 'file', new Error('not a regular file'))\n return\n }\n /* v8 ignore stop */\n } catch (err) {\n /* v8 ignore start -- stat TOCTOU failures are not deterministic to trigger */\n this.emitScanError(options, rel, 'file', err)\n return\n /* v8 ignore stop */\n }\n }\n\n assertScanEntryLimit(out.length + 1, maxScanEntries)\n out.push({\n relativePath: rel,\n absolutePath: fullPath,\n modTimeMillis: Math.floor(s.mtimeMs),\n size: s.size,\n fileIdentity: localFileIdentityFromStats(s),\n })\n }\n\n private async handleSymlink(\n root: string,\n fullPath: string,\n rel: string,\n out: LocalSyncPath[],\n options: SyncScanOptions,\n maxScanEntries: number,\n nodeDeps: LocalNodeDeps,\n activeDirectoryIds: Set<string>,\n followedDirectoryIds: Set<string>,\n ): Promise<void> {\n if ((options.localSymlinks ?? 'skip') === 'skip' || options.requireLocalSafePaths === true) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'local-symlink',\n message: `Skipped local path ${JSON.stringify(rel)}: local symlinks are disabled`,\n })\n return\n }\n\n let targetPath: string\n let targetStats: LocalStats\n try {\n targetPath = await nodeDeps.realpath(fullPath)\n targetStats = await nodeDeps.stat(targetPath)\n } catch (err) {\n this.emitScanError(options, rel, 'symlink', err)\n return\n }\n\n if (!isPathInsideRoot(root, targetPath, nodeDeps)) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'local-symlink',\n message: `Skipped local path ${JSON.stringify(rel)}: symlink target is outside the sync root`,\n })\n return\n }\n\n if (targetStats.isDirectory()) {\n if (directoryMayContainSyncPaths(rel, options)) {\n const identity = localNodeIdentity(targetStats)\n if (followedDirectoryIds.has(identity)) {\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'local-symlink-loop',\n message: `Skipped local path ${JSON.stringify(rel)}: symlink target was already visited`,\n })\n return\n }\n followedDirectoryIds.add(identity)\n await this.walkDirectory(\n root,\n targetPath,\n rel,\n out,\n options,\n maxScanEntries,\n nodeDeps,\n activeDirectoryIds,\n followedDirectoryIds,\n targetStats,\n )\n }\n return\n }\n\n if (targetStats.isFile()) {\n await this.collectFile(targetPath, rel, out, options, maxScanEntries, nodeDeps, targetStats)\n return\n }\n\n emitScannerSkip(options, {\n type: 'skip',\n path: rel,\n size: 0,\n reason: 'local-symlink',\n message: `Skipped local path ${JSON.stringify(rel)}: symlink target is not a regular file or directory`,\n })\n }\n\n private emitScanError(\n options: SyncScanOptions,\n path: string,\n kind: 'directory' | 'file' | 'symlink',\n err: unknown,\n ): Error {\n const event: SyncErrorEvent = {\n type: 'error',\n path,\n size: 0,\n message: `failed to scan local ${kind}: ${localFilesystemErrorReason(err)}`,\n }\n options.onError?.(event)\n return new Error(event.message)\n }\n}\n\nasync function loadLocalNodeDeps(): Promise<LocalNodeDeps> {\n const [fsPromises, path] = await Promise.all([import('node:fs/promises'), import('node:path')])\n return {\n readdir: fsPromises.readdir as LocalNodeDeps['readdir'],\n lstat: fsPromises.lstat as LocalNodeDeps['lstat'],\n stat: fsPromises.stat as LocalNodeDeps['stat'],\n join: path.join,\n relative: path.relative,\n realpath: fsPromises.realpath,\n resolve: path.resolve,\n isAbsolute: path.isAbsolute,\n sep: path.sep,\n }\n}\n\nfunction resolvePathAtConstruction(root: string): string {\n const processLike = (\n globalThis as {\n process?: { cwd?: () => string; platform?: string }\n }\n ).process\n if (typeof processLike?.cwd !== 'function') return root\n\n const cwd = processLike.cwd()\n if (processLike.platform === 'win32') return resolveWindowsPath(cwd, root)\n return resolvePosixPath(cwd, root)\n}\n\nfunction resolvePosixPath(cwd: string, root: string): string {\n const joined = root.startsWith('/') ? root : `${cwd}/${root}`\n const resolved = normalizePathSegments(joined.split('/'), '/')\n return resolved === '' ? '/' : `/${resolved}`\n}\n\nfunction resolveWindowsPath(cwd: string, root: string): string {\n const normalizedRoot = root.replaceAll('/', '\\\\')\n const normalizedCwd = cwd.replaceAll('/', '\\\\')\n const drive = /^[A-Za-z]:/.exec(normalizedCwd)?.[0] ?? ''\n const cwdUnc = splitUncPath(normalizedCwd)\n if (/^\\\\\\\\/.test(normalizedRoot)) return normalizeUncPath(normalizedRoot)\n if (/^[A-Za-z]:\\\\/.test(normalizedRoot)) {\n const prefix = normalizedRoot.slice(0, 2)\n const rest = normalizedRoot.slice(3).split('\\\\')\n return joinWindowsRoot(prefix, normalizePathSegments(rest, '\\\\'))\n }\n if (/^[A-Za-z]:/.test(normalizedRoot)) {\n throw new Error('LocalFolder root must not be a drive-relative Windows path')\n }\n if (normalizedRoot.startsWith('\\\\')) {\n const rest = normalizePathSegments(normalizedRoot.slice(1).split('\\\\'), '\\\\')\n return joinWindowsRoot(cwdUnc?.prefix ?? drive, rest)\n }\n if (cwdUnc !== undefined) {\n const resolved = normalizePathSegments([...cwdUnc.rest, ...normalizedRoot.split('\\\\')], '\\\\')\n return joinWindowsRoot(cwdUnc.prefix, resolved)\n }\n const base = /^[A-Za-z]:\\\\/.test(normalizedCwd) ? normalizedCwd : `${drive}\\\\`\n const prefix = /^[A-Za-z]:/.exec(base)?.[0] ?? drive\n const baseRest = base.slice(prefix.length).replace(/^\\\\/, '').split('\\\\')\n const resolved = normalizePathSegments([...baseRest, ...normalizedRoot.split('\\\\')], '\\\\')\n return joinWindowsRoot(prefix, resolved)\n}\n\nfunction normalizeUncPath(path: string): string {\n const unc = splitUncPath(path)\n if (unc === undefined) return path\n return joinWindowsRoot(unc.prefix, normalizePathSegments(unc.rest, '\\\\'))\n}\n\nfunction splitUncPath(\n path: string,\n): { readonly prefix: string; readonly rest: string[] } | undefined {\n if (!path.startsWith('\\\\\\\\')) return undefined\n const parts = path.split('\\\\').filter((part) => part !== '')\n const [server, share, ...rest] = parts\n if (server === undefined || share === undefined) return undefined\n return { prefix: `\\\\\\\\${server}\\\\${share}`, rest }\n}\n\nfunction joinWindowsRoot(prefix: string, rest: string): string {\n return rest === '' ? `${prefix}\\\\` : `${prefix}\\\\${rest}`\n}\n\nfunction normalizePathSegments(segments: readonly string[], separator: '/' | '\\\\'): string {\n const out: string[] = []\n for (const segment of segments) {\n if (segment === '' || segment === '.') continue\n if (segment === '..') {\n out.pop()\n continue\n }\n out.push(segment)\n }\n return out.join(separator)\n}\n\nfunction joinSyncRelativePath(parent: string, child: string): string {\n return parent === '' ? child : `${parent}/${child}`\n}\n\nfunction isPathInsideRoot(root: string, path: string, nodeDeps: LocalNodeDeps): boolean {\n const rel = nodeDeps.relative(root, path)\n return (\n rel === '' ||\n (!rel.startsWith(`..${nodeDeps.sep}`) && rel !== '..' && !nodeDeps.isAbsolute(rel))\n )\n}\n\nfunction localNodeIdentity(stats: LocalStats): string {\n return `${stats.dev}:${stats.ino}`\n}\n\nfunction throwIfScanAborted(options: SyncScanOptions): void {\n if (options.signal?.aborted === true) {\n throw options.signal.reason ?? new DOMException('Aborted', 'AbortError')\n }\n}\n"],"mappings":"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"}
@@ -28,12 +28,18 @@ export declare class LocalFolder implements SyncFolder {
28
28
  * Recursively collects files from {@link dir} into {@link out}.
29
29
  * @param root - Resolved scan root used for relative path calculation.
30
30
  * @param dir - Absolute path of the directory to scan.
31
+ * @param dirRel - Sync-relative path represented by {@link dir}.
31
32
  * @param out - Accumulator array that receives discovered file entries.
32
33
  * @param options - Optional scan controls.
33
34
  * @param maxScanEntries - Maximum number of entries to retain before failing.
34
35
  * @param nodeDeps - Lazy-loaded Node filesystem and path helpers.
36
+ * @param activeDirectoryIds - Directory identities in the current recursion stack.
37
+ * @param followedDirectoryIds - Followed symlink directory identities already traversed.
35
38
  */
36
39
  private walk;
40
+ private walkDirectory;
41
+ private collectFile;
42
+ private handleSymlink;
37
43
  private emitScanError;
38
44
  }
39
45
  //# sourceMappingURL=local.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/local.js"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAkB,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AA0B7F;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC5C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;IAChC,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;gBACS,IAAI,EAAE,MAAM;IAKxB;;;OAGG;IACI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,aAAa,CAAC;IAazE;;;;;;;;OAQG;YACW,IAAI;IA8FlB,OAAO,CAAC,aAAa;CAetB"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/sync/scanners/local.js"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAkB,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AA+B7F;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC5C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAS;IAChC,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,QAAQ,CAAC,kBAAkB,EAAG,IAAI,CAAS;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;gBACS,IAAI,EAAE,MAAM;IAKxB;;;OAGG;IACI,IAAI,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAAC,aAAa,CAAC;IAqCzE;;;;;;;;;;;OAWG;YACW,IAAI;YAuFJ,aAAa;YA4Db,WAAW;YA4CX,aAAa;IAuF3B,OAAO,CAAC,aAAa;CAetB"}
@@ -28,12 +28,18 @@ export declare class LocalFolder implements SyncFolder {
28
28
  * Recursively collects files from {@link dir} into {@link out}.
29
29
  * @param root - Resolved scan root used for relative path calculation.
30
30
  * @param dir - Absolute path of the directory to scan.
31
+ * @param dirRel - Sync-relative path represented by {@link dir}.
31
32
  * @param out - Accumulator array that receives discovered file entries.
32
33
  * @param options - Optional scan controls.
33
34
  * @param maxScanEntries - Maximum number of entries to retain before failing.
34
35
  * @param nodeDeps - Lazy-loaded Node filesystem and path helpers.
36
+ * @param activeDirectoryIds - Directory identities in the current recursion stack.
37
+ * @param followedDirectoryIds - Followed symlink directory identities already traversed.
35
38
  */
36
39
  private walk;
40
+ private walkDirectory;
41
+ private collectFile;
42
+ private handleSymlink;
37
43
  private emitScanError;
38
44
  }
39
45
  //# sourceMappingURL=local.d.ts.map