@milaboratories/pl-drivers 1.16.0 → 1.16.2

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 (243) hide show
  1. package/dist/clients/constructors.cjs.map +1 -1
  2. package/dist/clients/constructors.js.map +1 -1
  3. package/dist/clients/crc32c.cjs.map +1 -1
  4. package/dist/clients/crc32c.js.map +1 -1
  5. package/dist/clients/download.cjs +56 -5
  6. package/dist/clients/download.cjs.map +1 -1
  7. package/dist/clients/download.d.ts +13 -1
  8. package/dist/clients/download.d.ts.map +1 -1
  9. package/dist/clients/download.js +53 -2
  10. package/dist/clients/download.js.map +1 -1
  11. package/dist/clients/download_url_cache.cjs +3 -1
  12. package/dist/clients/download_url_cache.cjs.map +1 -1
  13. package/dist/clients/download_url_cache.js +2 -1
  14. package/dist/clients/download_url_cache.js.map +1 -1
  15. package/dist/clients/logs.cjs +2 -1
  16. package/dist/clients/logs.cjs.map +1 -1
  17. package/dist/clients/logs.d.ts.map +1 -1
  18. package/dist/clients/logs.js +2 -0
  19. package/dist/clients/logs.js.map +1 -1
  20. package/dist/clients/ls_api.cjs +1 -1
  21. package/dist/clients/ls_api.cjs.map +1 -1
  22. package/dist/clients/ls_api.d.ts.map +1 -1
  23. package/dist/clients/ls_api.js +1 -0
  24. package/dist/clients/ls_api.js.map +1 -1
  25. package/dist/clients/progress.cjs +2 -1
  26. package/dist/clients/progress.cjs.map +1 -1
  27. package/dist/clients/progress.d.ts.map +1 -1
  28. package/dist/clients/progress.js +2 -0
  29. package/dist/clients/progress.js.map +1 -1
  30. package/dist/clients/upload.cjs +7 -5
  31. package/dist/clients/upload.cjs.map +1 -1
  32. package/dist/clients/upload.d.ts.map +1 -1
  33. package/dist/clients/upload.js +6 -4
  34. package/dist/clients/upload.js.map +1 -1
  35. package/dist/drivers/download_blob/blob_key.cjs +1 -1
  36. package/dist/drivers/download_blob/blob_key.cjs.map +1 -1
  37. package/dist/drivers/download_blob/blob_key.js.map +1 -1
  38. package/dist/drivers/download_blob/download_blob.cjs +36 -7
  39. package/dist/drivers/download_blob/download_blob.cjs.map +1 -1
  40. package/dist/drivers/download_blob/download_blob.d.ts +9 -1
  41. package/dist/drivers/download_blob/download_blob.d.ts.map +1 -1
  42. package/dist/drivers/download_blob/download_blob.js +30 -1
  43. package/dist/drivers/download_blob/download_blob.js.map +1 -1
  44. package/dist/drivers/download_blob/download_blob_task.cjs +9 -3
  45. package/dist/drivers/download_blob/download_blob_task.cjs.map +1 -1
  46. package/dist/drivers/download_blob/download_blob_task.js +6 -1
  47. package/dist/drivers/download_blob/download_blob_task.js.map +1 -1
  48. package/dist/drivers/download_blob/sparse_cache/cache.cjs +9 -1
  49. package/dist/drivers/download_blob/sparse_cache/cache.cjs.map +1 -1
  50. package/dist/drivers/download_blob/sparse_cache/cache.js +8 -0
  51. package/dist/drivers/download_blob/sparse_cache/cache.js.map +1 -1
  52. package/dist/drivers/download_blob/sparse_cache/file.cjs +2 -1
  53. package/dist/drivers/download_blob/sparse_cache/file.cjs.map +1 -1
  54. package/dist/drivers/download_blob/sparse_cache/file.js +1 -1
  55. package/dist/drivers/download_blob/sparse_cache/file.js.map +1 -1
  56. package/dist/drivers/download_blob/sparse_cache/ranges.cjs +3 -1
  57. package/dist/drivers/download_blob/sparse_cache/ranges.cjs.map +1 -1
  58. package/dist/drivers/download_blob/sparse_cache/ranges.js +1 -1
  59. package/dist/drivers/download_blob/sparse_cache/ranges.js.map +1 -1
  60. package/dist/drivers/download_blob_url/driver.cjs +6 -1
  61. package/dist/drivers/download_blob_url/driver.cjs.map +1 -1
  62. package/dist/drivers/download_blob_url/driver.d.ts.map +1 -1
  63. package/dist/drivers/download_blob_url/driver.js +5 -0
  64. package/dist/drivers/download_blob_url/driver.js.map +1 -1
  65. package/dist/drivers/download_blob_url/driver_id.cjs +0 -1
  66. package/dist/drivers/download_blob_url/driver_id.cjs.map +1 -1
  67. package/dist/drivers/download_blob_url/driver_id.js.map +1 -1
  68. package/dist/drivers/download_blob_url/snapshot.cjs +0 -1
  69. package/dist/drivers/download_blob_url/snapshot.cjs.map +1 -1
  70. package/dist/drivers/download_blob_url/snapshot.d.ts +1 -2
  71. package/dist/drivers/download_blob_url/snapshot.d.ts.map +1 -1
  72. package/dist/drivers/download_blob_url/snapshot.js.map +1 -1
  73. package/dist/drivers/download_blob_url/task.cjs +14 -6
  74. package/dist/drivers/download_blob_url/task.cjs.map +1 -1
  75. package/dist/drivers/download_blob_url/task.d.ts +1 -1
  76. package/dist/drivers/download_blob_url/task.d.ts.map +1 -1
  77. package/dist/drivers/download_blob_url/task.js +8 -1
  78. package/dist/drivers/download_blob_url/task.js.map +1 -1
  79. package/dist/drivers/download_url/driver.cjs +41 -19
  80. package/dist/drivers/download_url/driver.cjs.map +1 -1
  81. package/dist/drivers/download_url/driver.d.ts +10 -5
  82. package/dist/drivers/download_url/driver.d.ts.map +1 -1
  83. package/dist/drivers/download_url/driver.js +40 -18
  84. package/dist/drivers/download_url/driver.js.map +1 -1
  85. package/dist/drivers/download_url/task.cjs +39 -23
  86. package/dist/drivers/download_url/task.cjs.map +1 -1
  87. package/dist/drivers/download_url/task.d.ts +20 -6
  88. package/dist/drivers/download_url/task.d.ts.map +1 -1
  89. package/dist/drivers/download_url/task.js +36 -20
  90. package/dist/drivers/download_url/task.js.map +1 -1
  91. package/dist/drivers/helpers/download_local_handle.cjs.map +1 -1
  92. package/dist/drivers/helpers/download_local_handle.js.map +1 -1
  93. package/dist/drivers/helpers/download_remote_handle.cjs +0 -1
  94. package/dist/drivers/helpers/download_remote_handle.cjs.map +1 -1
  95. package/dist/drivers/helpers/download_remote_handle.js.map +1 -1
  96. package/dist/drivers/helpers/files_cache.cjs +1 -1
  97. package/dist/drivers/helpers/files_cache.cjs.map +1 -1
  98. package/dist/drivers/helpers/files_cache.js +1 -0
  99. package/dist/drivers/helpers/files_cache.js.map +1 -1
  100. package/dist/drivers/helpers/helpers.cjs +1 -0
  101. package/dist/drivers/helpers/helpers.cjs.map +1 -1
  102. package/dist/drivers/helpers/helpers.d.ts.map +1 -1
  103. package/dist/drivers/helpers/helpers.js +1 -0
  104. package/dist/drivers/helpers/helpers.js.map +1 -1
  105. package/dist/drivers/helpers/logs_handle.cjs +3 -1
  106. package/dist/drivers/helpers/logs_handle.cjs.map +1 -1
  107. package/dist/drivers/helpers/logs_handle.js +1 -1
  108. package/dist/drivers/helpers/logs_handle.js.map +1 -1
  109. package/dist/drivers/helpers/ls_remote_import_handle.cjs.map +1 -1
  110. package/dist/drivers/helpers/ls_remote_import_handle.js.map +1 -1
  111. package/dist/drivers/helpers/ls_storage_entry.cjs +2 -1
  112. package/dist/drivers/helpers/ls_storage_entry.cjs.map +1 -1
  113. package/dist/drivers/helpers/ls_storage_entry.js +1 -1
  114. package/dist/drivers/helpers/ls_storage_entry.js.map +1 -1
  115. package/dist/drivers/helpers/polling_ops.d.ts.map +1 -1
  116. package/dist/drivers/helpers/read_file.cjs +2 -2
  117. package/dist/drivers/helpers/read_file.cjs.map +1 -1
  118. package/dist/drivers/helpers/read_file.js.map +1 -1
  119. package/dist/drivers/logs.cjs +3 -1
  120. package/dist/drivers/logs.cjs.map +1 -1
  121. package/dist/drivers/logs.js +3 -0
  122. package/dist/drivers/logs.js.map +1 -1
  123. package/dist/drivers/logs_stream.cjs +8 -1
  124. package/dist/drivers/logs_stream.cjs.map +1 -1
  125. package/dist/drivers/logs_stream.d.ts.map +1 -1
  126. package/dist/drivers/logs_stream.js +8 -0
  127. package/dist/drivers/logs_stream.js.map +1 -1
  128. package/dist/drivers/ls.cjs +10 -3
  129. package/dist/drivers/ls.cjs.map +1 -1
  130. package/dist/drivers/ls.d.ts.map +1 -1
  131. package/dist/drivers/ls.js +8 -1
  132. package/dist/drivers/ls.js.map +1 -1
  133. package/dist/drivers/types.cjs +10 -1
  134. package/dist/drivers/types.cjs.map +1 -1
  135. package/dist/drivers/types.d.ts +3 -4
  136. package/dist/drivers/types.d.ts.map +1 -1
  137. package/dist/drivers/types.js +10 -0
  138. package/dist/drivers/types.js.map +1 -1
  139. package/dist/drivers/upload.cjs +5 -1
  140. package/dist/drivers/upload.cjs.map +1 -1
  141. package/dist/drivers/upload.d.ts.map +1 -1
  142. package/dist/drivers/upload.js +5 -0
  143. package/dist/drivers/upload.js.map +1 -1
  144. package/dist/drivers/upload_task.cjs +6 -1
  145. package/dist/drivers/upload_task.cjs.map +1 -1
  146. package/dist/drivers/upload_task.d.ts.map +1 -1
  147. package/dist/drivers/upload_task.js +5 -0
  148. package/dist/drivers/upload_task.js.map +1 -1
  149. package/dist/drivers/urls/url.cjs +1 -1
  150. package/dist/drivers/urls/url.cjs.map +1 -1
  151. package/dist/drivers/urls/url.js.map +1 -1
  152. package/dist/drivers/virtual_storages.cjs +3 -3
  153. package/dist/drivers/virtual_storages.cjs.map +1 -1
  154. package/dist/drivers/virtual_storages.d.ts.map +1 -1
  155. package/dist/drivers/virtual_storages.js.map +1 -1
  156. package/dist/helpers/download.cjs +1 -1
  157. package/dist/helpers/download.cjs.map +1 -1
  158. package/dist/helpers/download.d.ts.map +1 -1
  159. package/dist/helpers/download.js +1 -0
  160. package/dist/helpers/download.js.map +1 -1
  161. package/dist/helpers/download_errors.cjs.map +1 -1
  162. package/dist/helpers/download_errors.d.ts.map +1 -1
  163. package/dist/helpers/download_errors.js.map +1 -1
  164. package/dist/helpers/validate.cjs +1 -1
  165. package/dist/helpers/validate.cjs.map +1 -1
  166. package/dist/helpers/validate.d.ts.map +1 -1
  167. package/dist/helpers/validate.js.map +1 -1
  168. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.cjs +4 -2
  169. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.cjs.map +1 -1
  170. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.cjs +1 -1
  171. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.cjs.map +1 -1
  172. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.d.ts +3 -5
  173. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.d.ts.map +1 -1
  174. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.js +1 -0
  175. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.js.map +1 -1
  176. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.d.ts +1 -1
  177. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.d.ts.map +1 -1
  178. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.js +7 -3
  179. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.js.map +1 -1
  180. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.cjs +4 -2
  181. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.cjs.map +1 -1
  182. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.cjs +1 -1
  183. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.cjs.map +1 -1
  184. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.js +1 -0
  185. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.js.map +1 -1
  186. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.d.ts +1 -1
  187. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.d.ts.map +1 -1
  188. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.js +7 -3
  189. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.js.map +1 -1
  190. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.cjs +7 -3
  191. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.cjs.map +1 -1
  192. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.cjs +1 -1
  193. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.cjs.map +1 -1
  194. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.d.ts +3 -5
  195. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.d.ts.map +1 -1
  196. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.js +1 -0
  197. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.js.map +1 -1
  198. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.d.ts +1 -1
  199. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.d.ts.map +1 -1
  200. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.js +3 -3
  201. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.js.map +1 -1
  202. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.cjs +8 -5
  203. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.cjs.map +1 -1
  204. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.cjs +1 -1
  205. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.cjs.map +1 -1
  206. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.d.ts +3 -5
  207. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.d.ts.map +1 -1
  208. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.js +1 -0
  209. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.js.map +1 -1
  210. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.d.ts.map +1 -1
  211. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.js +5 -5
  212. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.js.map +1 -1
  213. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.cjs +17 -7
  214. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.cjs.map +1 -1
  215. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client.cjs +1 -1
  216. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client.cjs.map +1 -1
  217. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client.js +1 -0
  218. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client.js.map +1 -1
  219. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.d.ts.map +1 -1
  220. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.js +7 -7
  221. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.js.map +1 -1
  222. package/dist/proto-grpc/google/protobuf/duration.cjs +0 -1
  223. package/dist/proto-grpc/google/protobuf/duration.cjs.map +1 -1
  224. package/dist/proto-grpc/google/protobuf/duration.d.ts.map +1 -1
  225. package/dist/proto-grpc/google/protobuf/duration.js.map +1 -1
  226. package/dist/proto-grpc/google/protobuf/timestamp.cjs +0 -1
  227. package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
  228. package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
  229. package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
  230. package/dist/proto-rest/downloadapi.d.ts +1 -1
  231. package/dist/proto-rest/downloadapi.d.ts.map +1 -1
  232. package/dist/proto-rest/index.d.ts +1 -1
  233. package/dist/proto-rest/index.d.ts.map +1 -1
  234. package/dist/proto-rest/progressapi.d.ts +1 -1
  235. package/dist/proto-rest/progressapi.d.ts.map +1 -1
  236. package/dist/proto-rest/streamingapi.d.ts +1 -1
  237. package/dist/proto-rest/streamingapi.d.ts.map +1 -1
  238. package/package.json +7 -7
  239. package/src/clients/download.ts +71 -3
  240. package/src/drivers/download_blob/download_blob.ts +30 -1
  241. package/src/drivers/download_url/driver.ts +44 -19
  242. package/src/drivers/download_url/get_local_tgz.test.ts +106 -0
  243. package/src/drivers/download_url/task.ts +53 -30
@@ -0,0 +1,106 @@
1
+ import { ConsoleLoggerAdapter, HmacSha256Signer } from "@milaboratories/ts-helpers";
2
+ import * as os from "node:os";
3
+ import { createHash } from "node:crypto";
4
+ import { text } from "node:stream/consumers";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import * as fs from "node:fs";
8
+ import * as fsp from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import * as zlib from "node:zlib";
11
+ import * as tar from "tar-fs";
12
+ import { Agent } from "undici";
13
+ import { DownloadUrlDriver } from "./driver";
14
+ import { test, expect } from "vitest";
15
+
16
+ function genSigner() {
17
+ return new HmacSha256Signer(HmacSha256Signer.generateSecret());
18
+ }
19
+
20
+ /** Packs a tiny UI folder into a real gzipped `ui.tgz` at `tgzPath`. */
21
+ async function makeUiTgz(tgzPath: string): Promise<void> {
22
+ const srcDir = await fsp.mkdtemp(path.join(os.tmpdir(), "ui-src-"));
23
+ await fsp.writeFile(path.join(srcDir, "index.html"), "<html>hello local tgz</html>");
24
+ await fsp.writeFile(path.join(srcDir, "index.js"), "'use strict'; console.log('ui');");
25
+ await pipeline(tar.pack(srcDir), zlib.createGzip(), fs.createWriteStream(tgzPath));
26
+ }
27
+
28
+ test("getLocalTgz unpacks a local ui.tgz, serves a signed path, and caches by path:mtime", async () => {
29
+ const logger = new ConsoleLoggerAdapter();
30
+ const saveDir = await fsp.mkdtemp(path.join(os.tmpdir(), "local-tgz-save-"));
31
+ const tgzPath = path.join(await fsp.mkdtemp(path.join(os.tmpdir(), "local-tgz-src-")), "ui.tgz");
32
+ await makeUiTgz(tgzPath);
33
+
34
+ // local-tgz never touches the network; a bare dispatcher is enough.
35
+ const driver = new DownloadUrlDriver(logger, new Agent(), saveDir, genSigner());
36
+ const mtime = "1782150933623";
37
+
38
+ const c = driver.getLocalTgz(tgzPath, mtime);
39
+
40
+ const first = await c.getValue();
41
+ expect(first).toBeUndefined();
42
+
43
+ await c.awaitChange();
44
+
45
+ const result = await c.getValue();
46
+ expect(result?.error).toBeUndefined();
47
+ expect(result?.url).not.toBeUndefined();
48
+
49
+ // The served URL resolves to the unpacked index.html.
50
+ const u = new URL(result!.url!);
51
+ u.pathname = "index.html";
52
+ const indexPath = driver.getPathForBlockUI(u.toString());
53
+ const html = await text(Readable.toWeb(fs.createReadStream(indexPath)));
54
+ expect(html).toContain("hello local tgz");
55
+
56
+ // Cache key is sha256(path + ":" + mtime) — the extraction dir lives there.
57
+ const expectedDir = path.join(
58
+ saveDir,
59
+ createHash("sha256").update(`${tgzPath}:${mtime}`).digest("hex"),
60
+ );
61
+ expect(fs.existsSync(expectedDir)).toBe(true);
62
+
63
+ // A second call (same path:mtime) is a cache hit — same served folder, no
64
+ // re-unpack, refcount still held by the first computable.
65
+ const c2 = driver.getLocalTgz(tgzPath, mtime);
66
+ let hit = await c2.getValue();
67
+ if (hit === undefined) {
68
+ await c2.awaitChange();
69
+ hit = await c2.getValue();
70
+ }
71
+ expect(hit?.url).toBe(result!.url);
72
+
73
+ c.resetState();
74
+ c2.resetState();
75
+ await driver.releaseAll();
76
+ });
77
+
78
+ test("getLocalTgz re-derives the extraction dir when mtime changes", async () => {
79
+ const logger = new ConsoleLoggerAdapter();
80
+ const saveDir = await fsp.mkdtemp(path.join(os.tmpdir(), "local-tgz-save2-"));
81
+ const tgzPath = path.join(await fsp.mkdtemp(path.join(os.tmpdir(), "local-tgz-src2-")), "ui.tgz");
82
+ await makeUiTgz(tgzPath);
83
+
84
+ const driver = new DownloadUrlDriver(logger, new Agent(), saveDir, genSigner());
85
+
86
+ const dirFor = (mtime: string) =>
87
+ path.join(saveDir, createHash("sha256").update(`${tgzPath}:${mtime}`).digest("hex"));
88
+
89
+ const cA = driver.getLocalTgz(tgzPath, "1");
90
+ expect(await cA.getValue()).toBeUndefined();
91
+ await cA.awaitChange();
92
+ expect((await cA.getValue())?.url).not.toBeUndefined();
93
+
94
+ const cB = driver.getLocalTgz(tgzPath, "2");
95
+ expect(await cB.getValue()).toBeUndefined();
96
+ await cB.awaitChange();
97
+ expect((await cB.getValue())?.url).not.toBeUndefined();
98
+
99
+ expect(dirFor("1")).not.toBe(dirFor("2"));
100
+ expect(fs.existsSync(dirFor("1"))).toBe(true);
101
+ expect(fs.existsSync(dirFor("2"))).toBe(true);
102
+
103
+ cA.resetState();
104
+ cB.resetState();
105
+ await driver.releaseAll();
106
+ });
@@ -8,9 +8,11 @@ import {
8
8
  fileExists,
9
9
  notEmpty,
10
10
  } from "@milaboratories/ts-helpers";
11
+ import { createReadStream } from "node:fs";
11
12
  import * as fsp from "node:fs/promises";
12
13
  import * as path from "node:path";
13
- import { Transform, Writable } from "node:stream";
14
+ import { Readable, Transform, Writable } from "node:stream";
15
+ import type { ReadableStream } from "node:stream/web";
14
16
  import * as zlib from "node:zlib";
15
17
  import * as tar from "tar-fs";
16
18
  import type { RemoteFileDownloader } from "../../helpers/download";
@@ -18,7 +20,14 @@ import { isDownloadNetworkError400 } from "../../helpers/download_errors";
18
20
  import type { UrlResult } from "./driver";
19
21
  import { newBlockUIURL } from "../urls/url";
20
22
 
21
- /** Downloads and extracts an archive to a directory. */
23
+ /** Source an archive is fetched from before extraction: a remote URL or a
24
+ * local `.tgz` file on disk. Both flow through the same untar machinery. */
25
+ export type DownloadSource =
26
+ | { type: "url"; url: URL }
27
+ | { type: "local-tgz"; path: string; mtime: string };
28
+
29
+ /** Fetches (over HTTP or from a local file) and extracts an archive to a
30
+ * directory. `key` is the task's identity in the driver's task map and cache. */
22
31
  export class DownloadByUrlTask {
23
32
  readonly counter = new CallersCounter();
24
33
  readonly change = new ChangeSource();
@@ -30,14 +39,15 @@ export class DownloadByUrlTask {
30
39
  constructor(
31
40
  private readonly logger: MiLogger,
32
41
  readonly path: string,
33
- readonly url: URL,
42
+ readonly source: DownloadSource,
43
+ readonly key: string,
34
44
  readonly signer: Signer,
35
45
  readonly saveDir: string,
36
46
  ) {}
37
47
 
38
48
  public info() {
39
49
  return {
40
- url: this.url.toString(),
50
+ key: this.key,
41
51
  path: this.path,
42
52
  done: this.done,
43
53
  size: this.size,
@@ -52,9 +62,9 @@ export class DownloadByUrlTask {
52
62
 
53
63
  async download(clientDownload: RemoteFileDownloader, withGunzip: boolean) {
54
64
  try {
55
- const size = await this.downloadAndUntar(clientDownload, withGunzip, this.signalCtl.signal);
65
+ const size = await this.fetchAndUntar(clientDownload, withGunzip, this.signalCtl.signal);
56
66
  this.setDone(size);
57
- this.change.markChanged(`download of ${this.url} finished`);
67
+ this.change.markChanged(`download of ${this.key} finished`);
58
68
  } catch (e: unknown) {
59
69
  if (
60
70
  e instanceof URLAborted ||
@@ -62,7 +72,7 @@ export class DownloadByUrlTask {
62
72
  this.signalCtl.signal.aborted
63
73
  ) {
64
74
  this.setError(e);
65
- this.change.markChanged(`download of ${this.url} failed`);
75
+ this.change.markChanged(`download of ${this.key} failed`);
66
76
  // Just in case we were half-way extracting an archive.
67
77
  await rmRFDir(this.path);
68
78
  return;
@@ -72,7 +82,7 @@ export class DownloadByUrlTask {
72
82
  }
73
83
  }
74
84
 
75
- private async downloadAndUntar(
85
+ private async fetchAndUntar(
76
86
  clientDownload: RemoteFileDownloader,
77
87
  withGunzip: boolean,
78
88
  signal: AbortSignal,
@@ -83,28 +93,41 @@ export class DownloadByUrlTask {
83
93
  return await dirSize(this.path);
84
94
  }
85
95
 
86
- const size = await clientDownload.withContent(
87
- this.url.toString(),
88
- {},
89
- { signal },
90
- async (content, size) => {
91
- let processedContent = content;
92
- if (withGunzip) {
93
- const gunzip = Transform.toWeb(zlib.createGunzip());
94
- processedContent = content.pipeThrough(gunzip, { signal });
95
- }
96
-
97
- await createPathAtomically(this.logger, this.path, async (fPath: string) => {
98
- await fsp.mkdir(fPath); // throws if a directory already exists.
99
- const untar = Writable.toWeb(tar.extract(fPath));
100
- await processedContent.pipeTo(untar, { signal });
101
- });
102
-
103
- return size;
104
- },
105
- );
106
-
107
- return size;
96
+ if (this.source.type === "url") {
97
+ return await clientDownload.withContent(
98
+ this.source.url.toString(),
99
+ {},
100
+ { signal },
101
+ async (content, size) => {
102
+ await this.untarStream(content, withGunzip, signal);
103
+ return size;
104
+ },
105
+ );
106
+ }
107
+
108
+ // local-tgz: read the archive from disk and untar through the same path.
109
+ const content = Readable.toWeb(createReadStream(this.source.path)) as ReadableStream;
110
+ await this.untarStream(content, withGunzip, signal);
111
+ return await dirSize(this.path);
112
+ }
113
+
114
+ /** Optionally gunzips, then untars the content stream into `this.path`. */
115
+ private async untarStream(
116
+ content: ReadableStream,
117
+ withGunzip: boolean,
118
+ signal: AbortSignal,
119
+ ): Promise<void> {
120
+ let processedContent = content;
121
+ if (withGunzip) {
122
+ const gunzip = Transform.toWeb(zlib.createGunzip());
123
+ processedContent = content.pipeThrough(gunzip, { signal });
124
+ }
125
+
126
+ await createPathAtomically(this.logger, this.path, async (fPath: string) => {
127
+ await fsp.mkdir(fPath); // throws if a directory already exists.
128
+ const untar = Writable.toWeb(tar.extract(fPath));
129
+ await processedContent.pipeTo(untar, { signal });
130
+ });
108
131
  }
109
132
 
110
133
  getUrl(): UrlResult | undefined {