+817
-8
Cargo.lock
+817
-8
Cargo.lock
···
31
31
]
32
32
33
33
[[package]]
34
+
name = "adler"
35
+
version = "1.0.2"
36
+
source = "registry+https://github.com/rust-lang/crates.io-index"
37
+
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
38
+
39
+
[[package]]
34
40
name = "adler2"
35
41
version = "2.0.1"
36
42
source = "registry+https://github.com/rust-lang/crates.io-index"
···
58
64
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
59
65
60
66
[[package]]
67
+
name = "aligned-vec"
68
+
version = "0.6.4"
69
+
source = "registry+https://github.com/rust-lang/crates.io-index"
70
+
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
71
+
dependencies = [
72
+
"equator",
73
+
]
74
+
75
+
[[package]]
61
76
name = "alloc-no-stdlib"
62
77
version = "2.0.4"
63
78
source = "registry+https://github.com/rust-lang/crates.io-index"
···
79
94
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
80
95
dependencies = [
81
96
"libc",
97
+
]
98
+
99
+
[[package]]
100
+
name = "ansi_colours"
101
+
version = "1.2.3"
102
+
source = "registry+https://github.com/rust-lang/crates.io-index"
103
+
checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe"
104
+
dependencies = [
105
+
"rgb",
82
106
]
83
107
84
108
[[package]]
···
138
162
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
139
163
140
164
[[package]]
165
+
name = "arbitrary"
166
+
version = "1.4.2"
167
+
source = "registry+https://github.com/rust-lang/crates.io-index"
168
+
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
169
+
170
+
[[package]]
171
+
name = "arg_enum_proc_macro"
172
+
version = "0.3.4"
173
+
source = "registry+https://github.com/rust-lang/crates.io-index"
174
+
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
175
+
dependencies = [
176
+
"proc-macro2",
177
+
"quote",
178
+
"syn 2.0.106",
179
+
]
180
+
181
+
[[package]]
182
+
name = "arrayvec"
183
+
version = "0.7.6"
184
+
source = "registry+https://github.com/rust-lang/crates.io-index"
185
+
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
186
+
187
+
[[package]]
141
188
name = "ascii"
142
189
version = "1.1.0"
143
190
source = "registry+https://github.com/rust-lang/crates.io-index"
···
180
227
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
181
228
182
229
[[package]]
230
+
name = "av1-grain"
231
+
version = "0.2.4"
232
+
source = "registry+https://github.com/rust-lang/crates.io-index"
233
+
checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8"
234
+
dependencies = [
235
+
"anyhow",
236
+
"arrayvec",
237
+
"log",
238
+
"nom",
239
+
"num-rational",
240
+
"v_frame",
241
+
]
242
+
243
+
[[package]]
244
+
name = "avif-serialize"
245
+
version = "0.8.6"
246
+
source = "registry+https://github.com/rust-lang/crates.io-index"
247
+
checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f"
248
+
dependencies = [
249
+
"arrayvec",
250
+
]
251
+
252
+
[[package]]
183
253
name = "axum"
184
254
version = "0.8.6"
185
255
source = "registry+https://github.com/rust-lang/crates.io-index"
···
280
350
"addr2line",
281
351
"cfg-if",
282
352
"libc",
283
-
"miniz_oxide",
353
+
"miniz_oxide 0.8.9",
284
354
"object",
285
355
"rustc-demangle",
286
356
"windows-link 0.2.0",
···
324
394
version = "1.8.0"
325
395
source = "registry+https://github.com/rust-lang/crates.io-index"
326
396
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
397
+
398
+
[[package]]
399
+
name = "bit_field"
400
+
version = "0.10.3"
401
+
source = "registry+https://github.com/rust-lang/crates.io-index"
402
+
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
327
403
328
404
[[package]]
329
405
name = "bitflags"
···
332
408
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
333
409
334
410
[[package]]
411
+
name = "bitstream-io"
412
+
version = "2.6.0"
413
+
source = "registry+https://github.com/rust-lang/crates.io-index"
414
+
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
415
+
416
+
[[package]]
335
417
name = "block-buffer"
336
418
version = "0.10.4"
337
419
source = "registry+https://github.com/rust-lang/crates.io-index"
···
430
512
]
431
513
432
514
[[package]]
515
+
name = "built"
516
+
version = "0.7.7"
517
+
source = "registry+https://github.com/rust-lang/crates.io-index"
518
+
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
519
+
520
+
[[package]]
433
521
name = "bumpalo"
434
522
version = "3.19.0"
435
523
source = "registry+https://github.com/rust-lang/crates.io-index"
436
524
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
437
525
438
526
[[package]]
527
+
name = "bytemuck"
528
+
version = "1.24.0"
529
+
source = "registry+https://github.com/rust-lang/crates.io-index"
530
+
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
531
+
532
+
[[package]]
439
533
name = "byteorder"
440
534
version = "1.5.0"
441
535
source = "registry+https://github.com/rust-lang/crates.io-index"
442
536
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
537
+
538
+
[[package]]
539
+
name = "byteorder-lite"
540
+
version = "0.1.0"
541
+
source = "registry+https://github.com/rust-lang/crates.io-index"
542
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
443
543
444
544
[[package]]
445
545
name = "bytes"
···
472
572
checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f"
473
573
dependencies = [
474
574
"find-msvc-tools",
575
+
"jobserver",
576
+
"libc",
475
577
"shlex",
476
578
]
477
579
···
491
593
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
492
594
493
595
[[package]]
596
+
name = "cfg-expr"
597
+
version = "0.15.8"
598
+
source = "registry+https://github.com/rust-lang/crates.io-index"
599
+
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
600
+
dependencies = [
601
+
"smallvec",
602
+
"target-lexicon",
603
+
]
604
+
605
+
[[package]]
494
606
name = "cfg-if"
495
607
version = "1.0.3"
496
608
source = "registry+https://github.com/rust-lang/crates.io-index"
···
604
716
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
605
717
606
718
[[package]]
719
+
name = "color_quant"
720
+
version = "1.1.0"
721
+
source = "registry+https://github.com/rust-lang/crates.io-index"
722
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
723
+
724
+
[[package]]
607
725
name = "colorchoice"
608
726
version = "1.0.4"
609
727
source = "registry+https://github.com/rust-lang/crates.io-index"
···
637
755
checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
638
756
639
757
[[package]]
758
+
name = "console"
759
+
version = "0.15.11"
760
+
source = "registry+https://github.com/rust-lang/crates.io-index"
761
+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
762
+
dependencies = [
763
+
"encode_unicode",
764
+
"libc",
765
+
"once_cell",
766
+
"windows-sys 0.59.0",
767
+
]
768
+
769
+
[[package]]
640
770
name = "const-oid"
641
771
version = "0.9.6"
642
772
source = "registry+https://github.com/rust-lang/crates.io-index"
···
706
836
]
707
837
708
838
[[package]]
839
+
name = "crossbeam-deque"
840
+
version = "0.8.6"
841
+
source = "registry+https://github.com/rust-lang/crates.io-index"
842
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
843
+
dependencies = [
844
+
"crossbeam-epoch",
845
+
"crossbeam-utils",
846
+
]
847
+
848
+
[[package]]
849
+
name = "crossbeam-epoch"
850
+
version = "0.9.18"
851
+
source = "registry+https://github.com/rust-lang/crates.io-index"
852
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
853
+
dependencies = [
854
+
"crossbeam-utils",
855
+
]
856
+
857
+
[[package]]
709
858
name = "crossbeam-utils"
710
859
version = "0.8.21"
711
860
source = "registry+https://github.com/rust-lang/crates.io-index"
712
861
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
713
862
714
863
[[package]]
864
+
name = "crossterm"
865
+
version = "0.28.1"
866
+
source = "registry+https://github.com/rust-lang/crates.io-index"
867
+
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
868
+
dependencies = [
869
+
"bitflags",
870
+
"crossterm_winapi",
871
+
"parking_lot",
872
+
"rustix 0.38.44",
873
+
"winapi",
874
+
]
875
+
876
+
[[package]]
877
+
name = "crossterm_winapi"
878
+
version = "0.9.1"
879
+
source = "registry+https://github.com/rust-lang/crates.io-index"
880
+
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
881
+
dependencies = [
882
+
"winapi",
883
+
]
884
+
885
+
[[package]]
715
886
name = "crunchy"
716
887
version = "0.2.4"
717
888
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1005
1176
]
1006
1177
1007
1178
[[package]]
1179
+
name = "encode_unicode"
1180
+
version = "1.0.0"
1181
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1182
+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
1183
+
1184
+
[[package]]
1008
1185
name = "encoding_rs"
1009
1186
version = "0.8.35"
1010
1187
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1020
1197
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
1021
1198
dependencies = [
1022
1199
"heck 0.5.0",
1200
+
"proc-macro2",
1201
+
"quote",
1202
+
"syn 2.0.106",
1203
+
]
1204
+
1205
+
[[package]]
1206
+
name = "equator"
1207
+
version = "0.4.2"
1208
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1209
+
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
1210
+
dependencies = [
1211
+
"equator-macro",
1212
+
]
1213
+
1214
+
[[package]]
1215
+
name = "equator-macro"
1216
+
version = "0.4.2"
1217
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1218
+
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
1219
+
dependencies = [
1023
1220
"proc-macro2",
1024
1221
"quote",
1025
1222
"syn 2.0.106",
···
1081
1278
]
1082
1279
1083
1280
[[package]]
1281
+
name = "exr"
1282
+
version = "1.73.0"
1283
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1284
+
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
1285
+
dependencies = [
1286
+
"bit_field",
1287
+
"half",
1288
+
"lebe",
1289
+
"miniz_oxide 0.8.9",
1290
+
"rayon-core",
1291
+
"smallvec",
1292
+
"zune-inflate",
1293
+
]
1294
+
1295
+
[[package]]
1084
1296
name = "fastrand"
1085
1297
version = "2.3.0"
1086
1298
source = "registry+https://github.com/rust-lang/crates.io-index"
1087
1299
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1300
+
1301
+
[[package]]
1302
+
name = "fax"
1303
+
version = "0.2.6"
1304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1305
+
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
1306
+
dependencies = [
1307
+
"fax_derive",
1308
+
]
1309
+
1310
+
[[package]]
1311
+
name = "fax_derive"
1312
+
version = "0.2.0"
1313
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1314
+
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
1315
+
dependencies = [
1316
+
"proc-macro2",
1317
+
"quote",
1318
+
"syn 2.0.106",
1319
+
]
1320
+
1321
+
[[package]]
1322
+
name = "fdeflate"
1323
+
version = "0.3.7"
1324
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1325
+
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
1326
+
dependencies = [
1327
+
"simd-adler32",
1328
+
]
1088
1329
1089
1330
[[package]]
1090
1331
name = "ff"
···
1127
1368
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
1128
1369
dependencies = [
1129
1370
"crc32fast",
1130
-
"miniz_oxide",
1371
+
"miniz_oxide 0.8.9",
1131
1372
]
1132
1373
1133
1374
[[package]]
···
1341
1582
"r-efi",
1342
1583
"wasip2",
1343
1584
"wasm-bindgen",
1585
+
]
1586
+
1587
+
[[package]]
1588
+
name = "gif"
1589
+
version = "0.13.3"
1590
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1591
+
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
1592
+
dependencies = [
1593
+
"color_quant",
1594
+
"weezl",
1344
1595
]
1345
1596
1346
1597
[[package]]
···
1765
2016
]
1766
2017
1767
2018
[[package]]
2019
+
name = "image"
2020
+
version = "0.25.8"
2021
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2022
+
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
2023
+
dependencies = [
2024
+
"bytemuck",
2025
+
"byteorder-lite",
2026
+
"color_quant",
2027
+
"exr",
2028
+
"gif",
2029
+
"image-webp",
2030
+
"moxcms",
2031
+
"num-traits",
2032
+
"png",
2033
+
"qoi",
2034
+
"ravif",
2035
+
"rayon",
2036
+
"rgb",
2037
+
"tiff 0.10.3",
2038
+
"zune-core",
2039
+
"zune-jpeg",
2040
+
]
2041
+
2042
+
[[package]]
2043
+
name = "image-webp"
2044
+
version = "0.2.4"
2045
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2046
+
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
2047
+
dependencies = [
2048
+
"byteorder-lite",
2049
+
"quick-error 2.0.1",
2050
+
]
2051
+
2052
+
[[package]]
2053
+
name = "imgref"
2054
+
version = "1.12.0"
2055
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2056
+
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
2057
+
2058
+
[[package]]
1768
2059
name = "indexmap"
1769
2060
version = "1.9.3"
1770
2061
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1792
2083
version = "2.0.6"
1793
2084
source = "registry+https://github.com/rust-lang/crates.io-index"
1794
2085
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
2086
+
2087
+
[[package]]
2088
+
name = "interpolate_name"
2089
+
version = "0.2.4"
2090
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2091
+
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
2092
+
dependencies = [
2093
+
"proc-macro2",
2094
+
"quote",
2095
+
"syn 2.0.106",
2096
+
]
1795
2097
1796
2098
[[package]]
1797
2099
name = "inventory"
···
1866
2168
1867
2169
[[package]]
1868
2170
name = "itertools"
2171
+
version = "0.12.1"
2172
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2173
+
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
2174
+
dependencies = [
2175
+
"either",
2176
+
]
2177
+
2178
+
[[package]]
2179
+
name = "itertools"
1869
2180
version = "0.14.0"
1870
2181
source = "registry+https://github.com/rust-lang/crates.io-index"
1871
2182
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
···
1886
2197
"bon",
1887
2198
"bytes",
1888
2199
"clap",
2200
+
"futures",
1889
2201
"getrandom 0.2.16",
1890
2202
"http",
2203
+
"image",
1891
2204
"jacquard-api 0.5.5",
1892
2205
"jacquard-common 0.5.4",
1893
2206
"jacquard-derive 0.5.4",
···
1895
2208
"jacquard-oauth",
1896
2209
"jose-jwk",
1897
2210
"miette",
2211
+
"n0-future",
1898
2212
"p256",
1899
2213
"percent-encoding",
1900
2214
"rand_core 0.6.4",
···
1905
2219
"serde_json",
1906
2220
"smol_str",
1907
2221
"thiserror 2.0.17",
2222
+
"tiff 0.6.1",
1908
2223
"tokio",
1909
2224
"tracing",
1910
2225
"trait-variant",
1911
2226
"url",
2227
+
"viuer",
1912
2228
]
1913
2229
1914
2230
[[package]]
···
2050
2366
source = "git+https://tangled.org/@nonbinary.computer/jacquard#77915fd4920b282b4b1342749dcdad9dce30cadf"
2051
2367
dependencies = [
2052
2368
"heck 0.5.0",
2053
-
"itertools",
2369
+
"itertools 0.14.0",
2054
2370
"prettyplease",
2055
2371
"proc-macro2",
2056
2372
"quote",
···
2107
2423
"jacquard-api 0.5.5",
2108
2424
"jacquard-common 0.5.4",
2109
2425
"miette",
2426
+
"n0-future",
2110
2427
"percent-encoding",
2111
2428
"reqwest",
2112
2429
"serde",
···
2163
2480
"jose-jwa",
2164
2481
"jose-jwk",
2165
2482
"miette",
2483
+
"n0-future",
2166
2484
"p256",
2167
2485
"rand 0.8.5",
2168
2486
"rand_core 0.6.4",
···
2205
2523
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
2206
2524
2207
2525
[[package]]
2526
+
name = "jobserver"
2527
+
version = "0.1.34"
2528
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2529
+
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
2530
+
dependencies = [
2531
+
"getrandom 0.3.4",
2532
+
"libc",
2533
+
]
2534
+
2535
+
[[package]]
2208
2536
name = "jose-b64"
2209
2537
version = "0.1.2"
2210
2538
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2241
2569
]
2242
2570
2243
2571
[[package]]
2572
+
name = "jpeg-decoder"
2573
+
version = "0.1.22"
2574
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2575
+
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
2576
+
2577
+
[[package]]
2244
2578
name = "js-sys"
2245
2579
version = "0.3.81"
2246
2580
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2272
2606
dependencies = [
2273
2607
"miette",
2274
2608
"num",
2275
-
"winnow",
2609
+
"winnow 0.6.24",
2276
2610
]
2277
2611
2278
2612
[[package]]
···
2296
2630
]
2297
2631
2298
2632
[[package]]
2633
+
name = "lebe"
2634
+
version = "0.5.3"
2635
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2636
+
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
2637
+
2638
+
[[package]]
2299
2639
name = "libc"
2300
2640
version = "0.2.176"
2301
2641
source = "registry+https://github.com/rust-lang/crates.io-index"
2302
2642
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
2303
2643
2304
2644
[[package]]
2645
+
name = "libfuzzer-sys"
2646
+
version = "0.4.10"
2647
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2648
+
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
2649
+
dependencies = [
2650
+
"arbitrary",
2651
+
"cc",
2652
+
]
2653
+
2654
+
[[package]]
2305
2655
name = "libm"
2306
2656
version = "0.2.15"
2307
2657
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2326
2676
2327
2677
[[package]]
2328
2678
name = "linux-raw-sys"
2679
+
version = "0.4.15"
2680
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2681
+
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
2682
+
2683
+
[[package]]
2684
+
name = "linux-raw-sys"
2329
2685
version = "0.11.0"
2330
2686
source = "registry+https://github.com/rust-lang/crates.io-index"
2331
2687
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
···
2365
2721
]
2366
2722
2367
2723
[[package]]
2724
+
name = "loop9"
2725
+
version = "0.1.5"
2726
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2727
+
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
2728
+
dependencies = [
2729
+
"imgref",
2730
+
]
2731
+
2732
+
[[package]]
2368
2733
name = "lru-cache"
2369
2734
version = "0.1.2"
2370
2735
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2380
2745
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
2381
2746
2382
2747
[[package]]
2748
+
name = "make-cmd"
2749
+
version = "0.1.0"
2750
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2751
+
checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
2752
+
2753
+
[[package]]
2383
2754
name = "malloc_buf"
2384
2755
version = "0.0.6"
2385
2756
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2404
2775
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
2405
2776
2406
2777
[[package]]
2778
+
name = "maybe-rayon"
2779
+
version = "0.1.1"
2780
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2781
+
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
2782
+
dependencies = [
2783
+
"cfg-if",
2784
+
"rayon",
2785
+
]
2786
+
2787
+
[[package]]
2407
2788
name = "memchr"
2408
2789
version = "2.7.6"
2409
2790
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2463
2844
2464
2845
[[package]]
2465
2846
name = "miniz_oxide"
2847
+
version = "0.4.4"
2848
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2849
+
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
2850
+
dependencies = [
2851
+
"adler",
2852
+
"autocfg",
2853
+
]
2854
+
2855
+
[[package]]
2856
+
name = "miniz_oxide"
2466
2857
version = "0.8.9"
2467
2858
source = "registry+https://github.com/rust-lang/crates.io-index"
2468
2859
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
2469
2860
dependencies = [
2470
2861
"adler2",
2862
+
"simd-adler32",
2471
2863
]
2472
2864
2473
2865
[[package]]
···
2482
2874
]
2483
2875
2484
2876
[[package]]
2877
+
name = "moxcms"
2878
+
version = "0.7.7"
2879
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2880
+
checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40"
2881
+
dependencies = [
2882
+
"num-traits",
2883
+
"pxfm",
2884
+
]
2885
+
2886
+
[[package]]
2485
2887
name = "multibase"
2486
2888
version = "0.9.1"
2487
2889
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2514
2916
"log",
2515
2917
"mime",
2516
2918
"mime_guess",
2517
-
"quick-error",
2919
+
"quick-error 1.2.3",
2518
2920
"rand 0.8.5",
2519
2921
"safemem",
2520
2922
"tempfile",
···
2549
2951
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
2550
2952
2551
2953
[[package]]
2954
+
name = "new_debug_unreachable"
2955
+
version = "1.0.6"
2956
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2957
+
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
2958
+
2959
+
[[package]]
2552
2960
name = "nom"
2553
2961
version = "7.1.3"
2554
2962
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2557
2965
"memchr",
2558
2966
"minimal-lexical",
2559
2967
]
2968
+
2969
+
[[package]]
2970
+
name = "noop_proc_macro"
2971
+
version = "0.3.0"
2972
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2973
+
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
2560
2974
2561
2975
[[package]]
2562
2976
name = "nu-ansi-term"
···
2622
3036
version = "0.1.0"
2623
3037
source = "registry+https://github.com/rust-lang/crates.io-index"
2624
3038
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
3039
+
3040
+
[[package]]
3041
+
name = "num-derive"
3042
+
version = "0.4.2"
3043
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3044
+
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
3045
+
dependencies = [
3046
+
"proc-macro2",
3047
+
"quote",
3048
+
"syn 2.0.106",
3049
+
]
2625
3050
2626
3051
[[package]]
2627
3052
name = "num-integer"
···
2795
3220
]
2796
3221
2797
3222
[[package]]
3223
+
name = "paste"
3224
+
version = "1.0.15"
3225
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3226
+
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
3227
+
3228
+
[[package]]
2798
3229
name = "pem-rfc7468"
2799
3230
version = "0.7.0"
2800
3231
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2860
3291
dependencies = [
2861
3292
"der",
2862
3293
"spki",
3294
+
]
3295
+
3296
+
[[package]]
3297
+
name = "pkg-config"
3298
+
version = "0.3.32"
3299
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3300
+
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
3301
+
3302
+
[[package]]
3303
+
name = "png"
3304
+
version = "0.18.0"
3305
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3306
+
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
3307
+
dependencies = [
3308
+
"bitflags",
3309
+
"crc32fast",
3310
+
"fdeflate",
3311
+
"flate2",
3312
+
"miniz_oxide 0.8.9",
2863
3313
]
2864
3314
2865
3315
[[package]]
···
2994
3444
]
2995
3445
2996
3446
[[package]]
3447
+
name = "profiling"
3448
+
version = "1.0.17"
3449
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3450
+
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
3451
+
dependencies = [
3452
+
"profiling-procmacros",
3453
+
]
3454
+
3455
+
[[package]]
3456
+
name = "profiling-procmacros"
3457
+
version = "1.0.17"
3458
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3459
+
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
3460
+
dependencies = [
3461
+
"quote",
3462
+
"syn 2.0.106",
3463
+
]
3464
+
3465
+
[[package]]
3466
+
name = "pxfm"
3467
+
version = "0.1.25"
3468
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3469
+
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
3470
+
dependencies = [
3471
+
"num-traits",
3472
+
]
3473
+
3474
+
[[package]]
3475
+
name = "qoi"
3476
+
version = "0.4.1"
3477
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3478
+
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
3479
+
dependencies = [
3480
+
"bytemuck",
3481
+
]
3482
+
3483
+
[[package]]
2997
3484
name = "quick-error"
2998
3485
version = "1.2.3"
2999
3486
source = "registry+https://github.com/rust-lang/crates.io-index"
3000
3487
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
3488
+
3489
+
[[package]]
3490
+
name = "quick-error"
3491
+
version = "2.0.1"
3492
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3493
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
3001
3494
3002
3495
[[package]]
3003
3496
name = "quinn"
···
3135
3628
checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab"
3136
3629
3137
3630
[[package]]
3631
+
name = "rav1e"
3632
+
version = "0.7.1"
3633
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3634
+
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
3635
+
dependencies = [
3636
+
"arbitrary",
3637
+
"arg_enum_proc_macro",
3638
+
"arrayvec",
3639
+
"av1-grain",
3640
+
"bitstream-io",
3641
+
"built",
3642
+
"cfg-if",
3643
+
"interpolate_name",
3644
+
"itertools 0.12.1",
3645
+
"libc",
3646
+
"libfuzzer-sys",
3647
+
"log",
3648
+
"maybe-rayon",
3649
+
"new_debug_unreachable",
3650
+
"noop_proc_macro",
3651
+
"num-derive",
3652
+
"num-traits",
3653
+
"once_cell",
3654
+
"paste",
3655
+
"profiling",
3656
+
"rand 0.8.5",
3657
+
"rand_chacha 0.3.1",
3658
+
"simd_helpers",
3659
+
"system-deps",
3660
+
"thiserror 1.0.69",
3661
+
"v_frame",
3662
+
"wasm-bindgen",
3663
+
]
3664
+
3665
+
[[package]]
3666
+
name = "ravif"
3667
+
version = "0.11.20"
3668
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3669
+
checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b"
3670
+
dependencies = [
3671
+
"avif-serialize",
3672
+
"imgref",
3673
+
"loop9",
3674
+
"quick-error 2.0.1",
3675
+
"rav1e",
3676
+
"rayon",
3677
+
"rgb",
3678
+
]
3679
+
3680
+
[[package]]
3138
3681
name = "raw-window-handle"
3139
3682
version = "0.5.2"
3140
3683
source = "registry+https://github.com/rust-lang/crates.io-index"
3141
3684
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
3142
3685
3143
3686
[[package]]
3687
+
name = "rayon"
3688
+
version = "1.11.0"
3689
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3690
+
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
3691
+
dependencies = [
3692
+
"either",
3693
+
"rayon-core",
3694
+
]
3695
+
3696
+
[[package]]
3697
+
name = "rayon-core"
3698
+
version = "1.13.0"
3699
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3700
+
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
3701
+
dependencies = [
3702
+
"crossbeam-deque",
3703
+
"crossbeam-utils",
3704
+
]
3705
+
3706
+
[[package]]
3144
3707
name = "redox_syscall"
3145
3708
version = "0.5.18"
3146
3709
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3269
3832
]
3270
3833
3271
3834
[[package]]
3835
+
name = "rgb"
3836
+
version = "0.8.52"
3837
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3838
+
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
3839
+
dependencies = [
3840
+
"bytemuck",
3841
+
]
3842
+
3843
+
[[package]]
3272
3844
name = "ring"
3273
3845
version = "0.17.14"
3274
3846
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3364
3936
3365
3937
[[package]]
3366
3938
name = "rustix"
3939
+
version = "0.38.44"
3940
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3941
+
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
3942
+
dependencies = [
3943
+
"bitflags",
3944
+
"errno",
3945
+
"libc",
3946
+
"linux-raw-sys 0.4.15",
3947
+
"windows-sys 0.59.0",
3948
+
]
3949
+
3950
+
[[package]]
3951
+
name = "rustix"
3367
3952
version = "1.1.2"
3368
3953
source = "registry+https://github.com/rust-lang/crates.io-index"
3369
3954
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
···
3371
3956
"bitflags",
3372
3957
"errno",
3373
3958
"libc",
3374
-
"linux-raw-sys",
3959
+
"linux-raw-sys 0.11.0",
3375
3960
"windows-sys 0.60.2",
3376
3961
]
3377
3962
···
3599
4184
]
3600
4185
3601
4186
[[package]]
4187
+
name = "serde_spanned"
4188
+
version = "0.6.9"
4189
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4190
+
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
4191
+
dependencies = [
4192
+
"serde",
4193
+
]
4194
+
4195
+
[[package]]
3602
4196
name = "serde_urlencoded"
3603
4197
version = "0.7.1"
3604
4198
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3705
4299
]
3706
4300
3707
4301
[[package]]
4302
+
name = "simd-adler32"
4303
+
version = "0.3.7"
4304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4305
+
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
4306
+
4307
+
[[package]]
4308
+
name = "simd_helpers"
4309
+
version = "0.1.0"
4310
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4311
+
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
4312
+
dependencies = [
4313
+
"quote",
4314
+
]
4315
+
4316
+
[[package]]
4317
+
name = "sixel-rs"
4318
+
version = "0.3.3"
4319
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4320
+
checksum = "cfa95c014543113a192d906e5971d0c8d1e8b4cc1e61026539687a7016644ce5"
4321
+
dependencies = [
4322
+
"sixel-sys",
4323
+
]
4324
+
4325
+
[[package]]
4326
+
name = "sixel-sys"
4327
+
version = "0.3.1"
4328
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4329
+
checksum = "fb46e0cd5569bf910390844174a5a99d52dd40681fff92228d221d9f8bf87dea"
4330
+
dependencies = [
4331
+
"make-cmd",
4332
+
]
4333
+
4334
+
[[package]]
3708
4335
name = "slab"
3709
4336
version = "0.4.11"
3710
4337
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3908
4535
]
3909
4536
3910
4537
[[package]]
4538
+
name = "system-deps"
4539
+
version = "6.2.2"
4540
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4541
+
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
4542
+
dependencies = [
4543
+
"cfg-expr",
4544
+
"heck 0.5.0",
4545
+
"pkg-config",
4546
+
"toml",
4547
+
"version-compare",
4548
+
]
4549
+
4550
+
[[package]]
4551
+
name = "target-lexicon"
4552
+
version = "0.12.16"
4553
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4554
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
4555
+
4556
+
[[package]]
3911
4557
name = "tempfile"
3912
4558
version = "3.23.0"
3913
4559
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3916
4562
"fastrand",
3917
4563
"getrandom 0.3.4",
3918
4564
"once_cell",
3919
-
"rustix",
4565
+
"rustix 1.1.2",
3920
4566
"windows-sys 0.60.2",
3921
4567
]
3922
4568
3923
4569
[[package]]
4570
+
name = "termcolor"
4571
+
version = "1.4.1"
4572
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4573
+
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
4574
+
dependencies = [
4575
+
"winapi-util",
4576
+
]
4577
+
4578
+
[[package]]
3924
4579
name = "terminal_size"
3925
4580
version = "0.4.3"
3926
4581
source = "registry+https://github.com/rust-lang/crates.io-index"
3927
4582
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
3928
4583
dependencies = [
3929
-
"rustix",
4584
+
"rustix 1.1.2",
3930
4585
"windows-sys 0.60.2",
3931
4586
]
3932
4587
···
3999
4654
]
4000
4655
4001
4656
[[package]]
4657
+
name = "tiff"
4658
+
version = "0.6.1"
4659
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4660
+
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
4661
+
dependencies = [
4662
+
"jpeg-decoder",
4663
+
"miniz_oxide 0.4.4",
4664
+
"weezl",
4665
+
]
4666
+
4667
+
[[package]]
4668
+
name = "tiff"
4669
+
version = "0.10.3"
4670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4671
+
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
4672
+
dependencies = [
4673
+
"fax",
4674
+
"flate2",
4675
+
"half",
4676
+
"quick-error 2.0.1",
4677
+
"weezl",
4678
+
"zune-jpeg",
4679
+
]
4680
+
4681
+
[[package]]
4002
4682
name = "time"
4003
4683
version = "0.3.44"
4004
4684
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4154
4834
]
4155
4835
4156
4836
[[package]]
4837
+
name = "toml"
4838
+
version = "0.8.23"
4839
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4840
+
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
4841
+
dependencies = [
4842
+
"serde",
4843
+
"serde_spanned",
4844
+
"toml_datetime",
4845
+
"toml_edit",
4846
+
]
4847
+
4848
+
[[package]]
4849
+
name = "toml_datetime"
4850
+
version = "0.6.11"
4851
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4852
+
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
4853
+
dependencies = [
4854
+
"serde",
4855
+
]
4856
+
4857
+
[[package]]
4858
+
name = "toml_edit"
4859
+
version = "0.22.27"
4860
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4861
+
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
4862
+
dependencies = [
4863
+
"indexmap 2.11.4",
4864
+
"serde",
4865
+
"serde_spanned",
4866
+
"toml_datetime",
4867
+
"winnow 0.7.13",
4868
+
]
4869
+
4870
+
[[package]]
4157
4871
name = "tower"
4158
4872
version = "0.5.2"
4159
4873
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4438
5152
]
4439
5153
4440
5154
[[package]]
5155
+
name = "v_frame"
5156
+
version = "0.3.9"
5157
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5158
+
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
5159
+
dependencies = [
5160
+
"aligned-vec",
5161
+
"num-traits",
5162
+
"wasm-bindgen",
5163
+
]
5164
+
5165
+
[[package]]
4441
5166
name = "valuable"
4442
5167
version = "0.1.1"
4443
5168
source = "registry+https://github.com/rust-lang/crates.io-index"
4444
5169
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
5170
+
5171
+
[[package]]
5172
+
name = "version-compare"
5173
+
version = "0.2.0"
5174
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5175
+
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
4445
5176
4446
5177
[[package]]
4447
5178
name = "version_check"
4448
5179
version = "0.9.5"
4449
5180
source = "registry+https://github.com/rust-lang/crates.io-index"
4450
5181
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
5182
+
5183
+
[[package]]
5184
+
name = "viuer"
5185
+
version = "0.9.2"
5186
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5187
+
checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac"
5188
+
dependencies = [
5189
+
"ansi_colours",
5190
+
"base64 0.22.1",
5191
+
"console",
5192
+
"crossterm",
5193
+
"image",
5194
+
"lazy_static",
5195
+
"sixel-rs",
5196
+
"tempfile",
5197
+
"termcolor",
5198
+
]
4451
5199
4452
5200
[[package]]
4453
5201
name = "walkdir"
···
4615
5363
]
4616
5364
4617
5365
[[package]]
5366
+
name = "weezl"
5367
+
version = "0.1.10"
5368
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5369
+
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
5370
+
5371
+
[[package]]
4618
5372
name = "widestring"
4619
5373
version = "1.2.0"
4620
5374
source = "registry+https://github.com/rust-lang/crates.io-index"
4621
5375
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
4622
5376
4623
5377
[[package]]
5378
+
name = "winapi"
5379
+
version = "0.3.9"
5380
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5381
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
5382
+
dependencies = [
5383
+
"winapi-i686-pc-windows-gnu",
5384
+
"winapi-x86_64-pc-windows-gnu",
5385
+
]
5386
+
5387
+
[[package]]
5388
+
name = "winapi-i686-pc-windows-gnu"
5389
+
version = "0.4.0"
5390
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5391
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
5392
+
5393
+
[[package]]
4624
5394
name = "winapi-util"
4625
5395
version = "0.1.11"
4626
5396
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4628
5398
dependencies = [
4629
5399
"windows-sys 0.60.2",
4630
5400
]
5401
+
5402
+
[[package]]
5403
+
name = "winapi-x86_64-pc-windows-gnu"
5404
+
version = "0.4.0"
5405
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5406
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
4631
5407
4632
5408
[[package]]
4633
5409
name = "windows"
···
5086
5862
]
5087
5863
5088
5864
[[package]]
5865
+
name = "winnow"
5866
+
version = "0.7.13"
5867
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5868
+
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
5869
+
dependencies = [
5870
+
"memchr",
5871
+
]
5872
+
5873
+
[[package]]
5089
5874
name = "winreg"
5090
5875
version = "0.50.0"
5091
5876
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5219
6004
"quote",
5220
6005
"syn 2.0.106",
5221
6006
]
6007
+
6008
+
[[package]]
6009
+
name = "zune-core"
6010
+
version = "0.4.12"
6011
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6012
+
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
6013
+
6014
+
[[package]]
6015
+
name = "zune-inflate"
6016
+
version = "0.2.54"
6017
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6018
+
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
6019
+
dependencies = [
6020
+
"simd-adler32",
6021
+
]
6022
+
6023
+
[[package]]
6024
+
name = "zune-jpeg"
6025
+
version = "0.4.21"
6026
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6027
+
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
6028
+
dependencies = [
6029
+
"zune-core",
6030
+
]
+1
Cargo.toml
+1
Cargo.toml
+1
-1
crates/jacquard-common/Cargo.toml
+1
-1
crates/jacquard-common/Cargo.toml
···
42
42
tokio = { workspace = true, default-features = false, features = ["sync"] }
43
43
44
44
# Streaming support (optional)
45
-
n0-future = { version = "0.1", optional = true }
45
+
n0-future = { workspace = true, optional = true }
46
46
futures = { version = "0.3", optional = true }
47
47
tokio-tungstenite-wasm = { version = "0.4", optional = true }
48
48
genawaiter = { version = "0.99.1", features = ["futures03"] }
+44
-9
crates/jacquard-common/src/stream.rs
+44
-9
crates/jacquard-common/src/stream.rs
···
49
49
pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
50
50
51
51
/// Error type for streaming operations
52
-
#[derive(Debug)]
52
+
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
53
53
pub struct StreamError {
54
54
kind: StreamErrorKind,
55
+
#[source]
55
56
source: Option<BoxError>,
56
57
}
57
58
···
156
157
}
157
158
}
158
159
159
-
impl Error for StreamError {
160
-
fn source(&self) -> Option<&(dyn Error + 'static)> {
161
-
self.source
162
-
.as_ref()
163
-
.map(|e| e.as_ref() as &(dyn Error + 'static))
164
-
}
165
-
}
166
-
167
160
use bytes::Bytes;
168
161
use n0_future::stream::Boxed;
169
162
···
203
196
/// Convert into the inner boxed stream
204
197
pub fn into_inner(self) -> Boxed<Result<Bytes, StreamError>> {
205
198
self.inner
199
+
}
200
+
201
+
/// Split this stream into two streams that both receive all chunks
202
+
///
203
+
/// Chunks are cloned (cheaply via Bytes rc). Spawns a forwarder task.
204
+
/// Both returned streams will receive all chunks from the original stream.
205
+
/// The forwarder continues as long as at least one stream is alive.
206
+
/// If the underlying stream errors, both teed streams will end.
207
+
pub fn tee(self) -> (ByteStream, ByteStream) {
208
+
use futures::channel::mpsc;
209
+
use n0_future::StreamExt as _;
210
+
211
+
let (tx1, rx1) = mpsc::unbounded();
212
+
let (tx2, rx2) = mpsc::unbounded();
213
+
214
+
n0_future::task::spawn(async move {
215
+
let mut stream = self.inner;
216
+
while let Some(result) = stream.next().await {
217
+
match result {
218
+
Ok(chunk) => {
219
+
// Clone chunk (cheap - Bytes is rc'd)
220
+
let chunk2 = chunk.clone();
221
+
222
+
// Send to both channels, continue if at least one succeeds
223
+
let send1 = tx1.unbounded_send(Ok(chunk));
224
+
let send2 = tx2.unbounded_send(Ok(chunk2));
225
+
226
+
// Only stop if both channels are closed
227
+
if send1.is_err() && send2.is_err() {
228
+
break;
229
+
}
230
+
}
231
+
Err(_e) => {
232
+
// Underlying stream errored, stop forwarding.
233
+
// Both channels will close, ending both streams.
234
+
break;
235
+
}
236
+
}
237
+
}
238
+
});
239
+
240
+
(ByteStream::new(rx1), ByteStream::new(rx2))
206
241
}
207
242
}
208
243
+5
-2
crates/jacquard-common/src/types/blob.rs
+5
-2
crates/jacquard-common/src/types/blob.rs
···
1
-
use crate::{CowStr, IntoStatic, types::cid::Cid};
1
+
use crate::{
2
+
CowStr, IntoStatic,
3
+
types::cid::{Cid, CidLink},
4
+
};
2
5
#[allow(unused)]
3
6
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
4
7
use smol_str::ToSmolStr;
···
24
27
#[serde(rename_all = "camelCase")]
25
28
pub struct Blob<'b> {
26
29
/// CID (Content Identifier) reference to the blob data
27
-
pub r#ref: Cid<'b>,
30
+
pub r#ref: CidLink<'b>,
28
31
/// MIME type of the blob (e.g., "image/png", "video/mp4")
29
32
#[serde(borrow)]
30
33
pub mime_type: MimeType<'b>,
+2
-1
crates/jacquard-common/src/types/value/convert.rs
+2
-1
crates/jacquard-common/src/types/value/convert.rs
···
1
1
use crate::IntoStatic;
2
+
use crate::types::cid::CidLink;
2
3
use crate::types::{
3
4
DataModelType,
4
5
cid::Cid,
···
298
299
}
299
300
};
300
301
return Ok(Data::Blob(crate::types::blob::Blob {
301
-
r#ref: cid.clone(),
302
+
r#ref: CidLink::str(cid).into_static(),
302
303
mime_type: crate::types::blob::MimeType::from(mime.clone()),
303
304
size: size_val,
304
305
}));
+4
-4
crates/jacquard-common/src/types/value/parsing.rs
+4
-4
crates/jacquard-common/src/types/value/parsing.rs
···
251
251
});
252
252
if let (Some(mime_type), Some(size)) = (mime_type, size) {
253
253
return Some(Blob {
254
-
r#ref: Cid::ipld(*value),
254
+
r#ref: CidLink::ipld(*value),
255
255
mime_type: MimeType::raw(mime_type),
256
256
size: size as usize,
257
257
});
···
259
259
} else if let Some(Ipld::String(value)) = blob.get("cid") {
260
260
if let Some(mime_type) = mime_type {
261
261
return Some(Blob {
262
-
r#ref: Cid::str(value),
262
+
r#ref: CidLink::str(value),
263
263
mime_type: MimeType::raw(mime_type),
264
264
size: 0,
265
265
});
···
281
281
let size = blob.get("size").and_then(|v| v.as_u64());
282
282
if let (Some(mime_type), Some(size)) = (mime_type, size) {
283
283
return Some(Blob {
284
-
r#ref: Cid::str(value),
284
+
r#ref: CidLink::str(value),
285
285
mime_type: MimeType::raw(mime_type),
286
286
size: size as usize,
287
287
});
···
290
290
} else if let Some(value) = blob.get("cid").and_then(|v| v.as_str()) {
291
291
if let Some(mime_type) = mime_type {
292
292
return Some(Blob {
293
-
r#ref: Cid::str(value),
293
+
r#ref: CidLink::str(value),
294
294
mime_type: MimeType::raw(mime_type),
295
295
size: 0,
296
296
});
+2
-2
crates/jacquard-common/src/types/value/serde_impl.rs
+2
-2
crates/jacquard-common/src/types/value/serde_impl.rs
···
320
320
321
321
if let (Some(ref_cid), Some(mime_cowstr), Some(size)) = (ref_cid, mime_type, size) {
322
322
return Ok(Data::Blob(Blob {
323
-
r#ref: ref_cid,
323
+
r#ref: CidLink::str(ref_cid.as_str()).into_static(),
324
324
mime_type: MimeType::from(mime_cowstr),
325
325
size,
326
326
}));
···
749
749
750
750
if let (Some(ref_cid), Some(mime_cowstr), Some(size)) = (ref_cid, mime_type, size) {
751
751
return Ok(RawData::Blob(Blob {
752
-
r#ref: ref_cid,
752
+
r#ref: CidLink::str(ref_cid.as_str()).into_static(),
753
753
mime_type: MimeType::from(mime_cowstr),
754
754
size,
755
755
}));
+53
-7
crates/jacquard-common/src/xrpc.rs
+53
-7
crates/jacquard-common/src/xrpc.rs
···
15
15
16
16
use ipld_core::ipld::Ipld;
17
17
#[cfg(feature = "streaming")]
18
-
pub use streaming::StreamingResponse;
18
+
pub use streaming::{
19
+
StreamingResponse, XrpcProcedureSend, XrpcProcedureStream, XrpcResponseStream, XrpcStreamResp,
20
+
};
19
21
20
22
#[cfg(feature = "websocket")]
21
23
pub mod subscription;
···
44
46
use crate::{CowStr, error::XrpcResult};
45
47
use crate::{IntoStatic, error::DecodeError};
46
48
#[cfg(feature = "streaming")]
47
-
use crate::{
48
-
StreamError,
49
-
xrpc::streaming::{XrpcProcedureSend, XrpcProcedureStream, XrpcResponseStream, XrpcStreamResp},
50
-
};
49
+
use crate::StreamError;
51
50
use crate::{error::TransportError, types::value::RawData};
52
51
53
52
/// Error type for encoding XRPC requests
···
272
271
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
273
272
pub trait XrpcClient: HttpClient {
274
273
/// Get the base URI for the client.
275
-
fn base_uri(&self) -> Url;
274
+
fn base_uri(&self) -> impl Future<Output = Url>;
276
275
277
276
/// Get the call options for the client.
278
277
fn opts(&self) -> impl Future<Output = CallOptions<'_>> {
···
316
315
where
317
316
R: XrpcRequest + Send + Sync,
318
317
<R as XrpcRequest>::Response: Send + Sync;
318
+
319
+
}
320
+
321
+
/// Stateful XRPC streaming client trait
322
+
#[cfg(feature = "streaming")]
323
+
pub trait XrpcStreamingClient: XrpcClient + HttpClientExt {
324
+
/// Send an XRPC request and stream the response
325
+
#[cfg(not(target_arch = "wasm32"))]
326
+
fn download<R>(
327
+
&self,
328
+
request: R,
329
+
) -> impl Future<Output = Result<StreamingResponse, StreamError>> + Send
330
+
where
331
+
R: XrpcRequest + Send + Sync,
332
+
<R as XrpcRequest>::Response: Send + Sync,
333
+
Self: Sync;
334
+
335
+
/// Send an XRPC request and stream the response
336
+
#[cfg(target_arch = "wasm32")]
337
+
fn download<R>(
338
+
&self,
339
+
request: R,
340
+
) -> impl Future<Output = Result<StreamingResponse, StreamError>>
341
+
where
342
+
R: XrpcRequest + Send + Sync,
343
+
<R as XrpcRequest>::Response: Send + Sync;
344
+
345
+
/// Stream an XRPC procedure call and its response
346
+
#[cfg(not(target_arch = "wasm32"))]
347
+
fn stream<S>(
348
+
&self,
349
+
stream: XrpcProcedureSend<S::Frame<'static>>,
350
+
) -> impl Future<Output = Result<XrpcResponseStream<<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>>, StreamError>>
351
+
where
352
+
S: XrpcProcedureStream + 'static,
353
+
<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp,
354
+
Self: Sync;
355
+
356
+
/// Stream an XRPC procedure call and its response
357
+
#[cfg(target_arch = "wasm32")]
358
+
fn stream<S>(
359
+
&self,
360
+
stream: XrpcProcedureSend<S::Frame<'static>>,
361
+
) -> impl Future<Output = Result<XrpcResponseStream<<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>>, StreamError>>
362
+
where
363
+
S: XrpcProcedureStream + 'static,
364
+
<<S as XrpcProcedureStream>::Response as XrpcStreamResp>::Frame<'static>: XrpcStreamResp;
319
365
}
320
366
321
367
/// Stateless XRPC call builder.
···
947
993
/// Stream an XRPC procedure call and its response
948
994
///
949
995
/// Useful for streaming upload of large payloads, or for "pipe-through" operations
950
-
/// where you processing a large payload.
996
+
/// where you are processing a large payload.
951
997
pub async fn stream<S>(
952
998
self,
953
999
stream: XrpcProcedureSend<S::Frame<'static>>,
+1
-1
crates/jacquard-common/src/xrpc/streaming.rs
+1
-1
crates/jacquard-common/src/xrpc/streaming.rs
+3
-3
crates/jacquard-common/src/xrpc/subscription.rs
+3
-3
crates/jacquard-common/src/xrpc/subscription.rs
···
472
472
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
473
473
pub trait SubscriptionClient: WebSocketClient {
474
474
/// Get the base URI for the client.
475
-
fn base_uri(&self) -> Url;
475
+
fn base_uri(&self) -> impl Future<Output = Url>;
476
476
477
477
/// Get the subscription options for the client.
478
478
fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> {
···
570
570
}
571
571
572
572
impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> {
573
-
fn base_uri(&self) -> Url {
573
+
async fn base_uri(&self) -> Url {
574
574
self.base_uri.clone()
575
575
}
576
576
···
613
613
Sub: XrpcSubscription + Send + Sync,
614
614
Self: Sync,
615
615
{
616
-
let base = self.base_uri();
616
+
let base = self.base_uri().await;
617
617
self.subscription(base)
618
618
.with_options(opts)
619
619
.subscribe(params)
+2
-1
crates/jacquard-identity/Cargo.toml
+2
-1
crates/jacquard-identity/Cargo.toml
···
15
15
[features]
16
16
dns = ["dep:hickory-resolver"]
17
17
tracing = ["dep:tracing"]
18
+
streaming = ["jacquard-common/streaming", "dep:n0-future"]
18
19
19
20
[dependencies]
20
21
trait-variant.workspace = true
···
33
34
serde_html_form.workspace = true
34
35
urlencoding.workspace = true
35
36
tracing = { workspace = true, optional = true }
36
-
37
+
n0-future = { workspace = true, optional = true }
37
38
38
39
[target.'cfg(not(target_family = "wasm"))'.dependencies]
39
40
hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]}
+31
-1
crates/jacquard-identity/src/lib.rs
+31
-1
crates/jacquard-identity/src/lib.rs
···
77
77
use bytes::Bytes;
78
78
use jacquard_api::com_atproto::identity::resolve_did;
79
79
use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle;
80
+
#[cfg(feature = "streaming")]
81
+
use jacquard_common::ByteStream;
80
82
use jacquard_common::error::TransportError;
81
83
use jacquard_common::http_client::HttpClient;
82
84
use jacquard_common::types::did::Did;
···
89
91
use url::{ParseError, Url};
90
92
91
93
#[cfg(all(feature = "dns", not(target_family = "wasm")))]
92
-
use {hickory_resolver::{TokioAsyncResolver, config::ResolverConfig}, std::sync::Arc};
94
+
use {
95
+
hickory_resolver::{TokioAsyncResolver, config::ResolverConfig},
96
+
std::sync::Arc,
97
+
};
93
98
94
99
/// Default resolver implementation with configurable fallback order.
95
100
#[derive(Clone)]
···
499
504
}
500
505
501
506
type Error = reqwest::Error;
507
+
}
508
+
509
+
#[cfg(feature = "streaming")]
510
+
impl jacquard_common::http_client::HttpClientExt for JacquardResolver {
511
+
/// Send HTTP request and return streaming response
512
+
fn send_http_streaming(
513
+
&self,
514
+
request: http::Request<Vec<u8>>,
515
+
) -> impl Future<Output = Result<http::Response<ByteStream>, Self::Error>> {
516
+
self.http.send_http_streaming(request)
517
+
}
518
+
519
+
/// Send HTTP request with streaming body and receive streaming response
520
+
fn send_http_bidirectional<S>(
521
+
&self,
522
+
parts: http::request::Parts,
523
+
body: S,
524
+
) -> impl Future<Output = Result<http::Response<ByteStream>, Self::Error>>
525
+
where
526
+
S: n0_future::Stream<Item = Result<bytes::Bytes, jacquard_common::StreamError>>
527
+
+ Send
528
+
+ 'static,
529
+
{
530
+
self.http.send_http_bidirectional(parts, body)
531
+
}
502
532
}
503
533
504
534
/// Warnings produced during identity checks that are not fatal
+2
crates/jacquard-oauth/Cargo.toml
+2
crates/jacquard-oauth/Cargo.toml
···
37
37
tokio = { workspace = true, default-features = false, features = ["sync"] }
38
38
reqwest.workspace = true
39
39
trait-variant.workspace = true
40
+
n0-future = { workspace = true, optional = true }
40
41
webbrowser = { version = "0.8", optional = true }
41
42
tracing = { workspace = true, optional = true }
42
43
···
50
51
browser-open = ["dep:webbrowser"]
51
52
tracing = ["dep:tracing"]
52
53
websocket = ["jacquard-common/websocket"]
54
+
streaming = ["jacquard-common/streaming", "dep:n0-future"]
+196
-16
crates/jacquard-oauth/src/client.rs
+196
-16
crates/jacquard-oauth/src/client.rs
···
29
29
resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions},
30
30
};
31
31
use jose_jwk::JwkSet;
32
-
use std::sync::Arc;
32
+
use std::{future::Future, sync::Arc};
33
33
use tokio::sync::RwLock;
34
34
use url::Url;
35
35
···
458
458
T: OAuthResolver + DpopExt + XrpcExt + Send + Sync + 'static,
459
459
W: Send + Sync,
460
460
{
461
-
fn base_uri(&self) -> Url {
462
-
// base_uri is a synchronous trait method; we must avoid async `.read().await`.
463
-
// Use `block_in_place` under Tokio runtime to perform a blocking RwLock read safely.
464
-
#[cfg(not(target_arch = "wasm32"))]
465
-
if tokio::runtime::Handle::try_current().is_ok() {
466
-
return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone());
467
-
}
468
-
469
-
self.data.blocking_read().host_url.clone()
461
+
async fn base_uri(&self) -> Url {
462
+
self.data.read().await.host_url.clone()
470
463
}
471
464
472
465
async fn opts(&self) -> CallOptions<'_> {
···
491
484
R: XrpcRequest + Send + Sync,
492
485
<R as XrpcRequest>::Response: Send + Sync,
493
486
{
494
-
let base_uri = self.base_uri();
487
+
let base_uri = self.base_uri().await;
495
488
opts.auth = Some(self.access_token().await);
496
489
let guard = self.data.read().await;
497
490
let mut dpop = guard.dpop_data.clone();
···
524
517
}
525
518
}
526
519
520
+
#[cfg(feature = "streaming")]
521
+
impl<T, S, W> jacquard_common::http_client::HttpClientExt for OAuthSession<T, S, W>
522
+
where
523
+
S: ClientAuthStore + Send + Sync + 'static,
524
+
T: OAuthResolver
525
+
+ DpopExt
526
+
+ XrpcExt
527
+
+ jacquard_common::http_client::HttpClientExt
528
+
+ Send
529
+
+ Sync
530
+
+ 'static,
531
+
W: Send + Sync,
532
+
{
533
+
async fn send_http_streaming(
534
+
&self,
535
+
request: http::Request<Vec<u8>>,
536
+
) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error>
537
+
{
538
+
self.client.send_http_streaming(request).await
539
+
}
540
+
541
+
async fn send_http_bidirectional<Str>(
542
+
&self,
543
+
parts: http::request::Parts,
544
+
body: Str,
545
+
) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error>
546
+
where
547
+
Str: n0_future::Stream<
548
+
Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>,
549
+
> + Send
550
+
+ 'static,
551
+
{
552
+
self.client.send_http_bidirectional(parts, body).await
553
+
}
554
+
}
555
+
556
+
#[cfg(feature = "streaming")]
557
+
impl<T, S, W> jacquard_common::xrpc::XrpcStreamingClient for OAuthSession<T, S, W>
558
+
where
559
+
S: ClientAuthStore + Send + Sync + 'static,
560
+
T: OAuthResolver
561
+
+ DpopExt
562
+
+ XrpcExt
563
+
+ jacquard_common::http_client::HttpClientExt
564
+
+ Send
565
+
+ Sync
566
+
+ 'static,
567
+
W: Send + Sync,
568
+
{
569
+
async fn download<R>(
570
+
&self,
571
+
request: R,
572
+
) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError>
573
+
where
574
+
R: XrpcRequest + Send + Sync,
575
+
<R as XrpcRequest>::Response: Send + Sync,
576
+
{
577
+
use jacquard_common::StreamError;
578
+
579
+
let base_uri = <Self as XrpcClient>::base_uri(self).await;
580
+
let mut opts = self.options.read().await.clone();
581
+
opts.auth = Some(self.access_token().await);
582
+
let http_request = build_http_request(&base_uri, &request, &opts)
583
+
.map_err(|e| StreamError::protocol(e.to_string()))?;
584
+
let guard = self.data.read().await;
585
+
let mut dpop = guard.dpop_data.clone();
586
+
let result = self
587
+
.client
588
+
.dpop_call(&mut dpop)
589
+
.send_streaming(http_request)
590
+
.await;
591
+
drop(guard);
592
+
593
+
match result {
594
+
Ok(response) => Ok(response),
595
+
Err(_e) => {
596
+
// Check if it's an auth error and retry
597
+
opts.auth = Some(
598
+
self.refresh()
599
+
.await
600
+
.map_err(|e| StreamError::transport(e))?,
601
+
);
602
+
let http_request = build_http_request(&base_uri, &request, &opts)
603
+
.map_err(|e| StreamError::protocol(e.to_string()))?;
604
+
let guard = self.data.read().await;
605
+
let mut dpop = guard.dpop_data.clone();
606
+
self.client
607
+
.dpop_call(&mut dpop)
608
+
.send_streaming(http_request)
609
+
.await
610
+
.map_err(StreamError::transport)
611
+
}
612
+
}
613
+
}
614
+
615
+
async fn stream<Str>(
616
+
&self,
617
+
stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>,
618
+
) -> core::result::Result<
619
+
jacquard_common::xrpc::streaming::XrpcResponseStream<
620
+
<<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>,
621
+
>,
622
+
jacquard_common::StreamError,
623
+
>
624
+
where
625
+
Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static,
626
+
<<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp,
627
+
{
628
+
use jacquard_common::StreamError;
629
+
use n0_future::{StreamExt, TryStreamExt};
630
+
631
+
let base_uri = self.base_uri().await;
632
+
let mut opts = self.options.read().await.clone();
633
+
opts.auth = Some(self.access_token().await);
634
+
635
+
let mut url = base_uri;
636
+
let mut path = url.path().trim_end_matches('/').to_owned();
637
+
path.push_str("/xrpc/");
638
+
path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID);
639
+
url.set_path(&path);
640
+
641
+
let mut builder = http::Request::post(url.to_string());
642
+
643
+
if let Some(token) = &opts.auth {
644
+
use jacquard_common::AuthorizationToken;
645
+
let hv = match token {
646
+
AuthorizationToken::Bearer(t) => {
647
+
http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref()))
648
+
}
649
+
AuthorizationToken::Dpop(t) => {
650
+
http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref()))
651
+
}
652
+
}
653
+
.map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?;
654
+
builder = builder.header(http::header::AUTHORIZATION, hv);
655
+
}
656
+
657
+
if let Some(proxy) = &opts.atproto_proxy {
658
+
builder = builder.header("atproto-proxy", proxy.as_ref());
659
+
}
660
+
if let Some(labelers) = &opts.atproto_accept_labelers {
661
+
if !labelers.is_empty() {
662
+
let joined = labelers
663
+
.iter()
664
+
.map(|s| s.as_ref())
665
+
.collect::<Vec<_>>()
666
+
.join(", ");
667
+
builder = builder.header("atproto-accept-labelers", joined);
668
+
}
669
+
}
670
+
for (name, value) in &opts.extra_headers {
671
+
builder = builder.header(name, value);
672
+
}
673
+
674
+
let (parts, _) = builder
675
+
.body(())
676
+
.map_err(|e| StreamError::protocol(e.to_string()))?
677
+
.into_parts();
678
+
679
+
let body_stream =
680
+
jacquard_common::stream::ByteStream::new(stream.0.map_ok(|f| f.buffer).boxed());
681
+
682
+
let guard = self.data.read().await;
683
+
let mut dpop = guard.dpop_data.clone();
684
+
let result = self
685
+
.client
686
+
.dpop_call(&mut dpop)
687
+
.send_bidirectional(parts, body_stream)
688
+
.await;
689
+
drop(guard);
690
+
691
+
match result {
692
+
Ok(response) => {
693
+
let (resp_parts, resp_body) = response.into_parts();
694
+
Ok(
695
+
jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts(
696
+
resp_parts, resp_body,
697
+
),
698
+
)
699
+
}
700
+
Err(e) => {
701
+
// OAuth token refresh and retry is handled by dpop wrapper
702
+
// If we get here, it's a real error
703
+
Err(StreamError::transport(e))
704
+
}
705
+
}
706
+
}
707
+
}
708
+
527
709
fn is_invalid_token_response<R: XrpcResp>(response: &XrpcResult<Response<R>>) -> bool {
528
710
match response {
529
711
Err(ClientError::Auth(AuthError::InvalidToken)) => true,
···
592
774
T: OAuthResolver + Send + Sync + 'static,
593
775
W: WebSocketClient + Send + Sync,
594
776
{
595
-
fn base_uri(&self) -> Url {
777
+
async fn base_uri(&self) -> Url {
596
778
#[cfg(not(target_arch = "wasm32"))]
597
779
if tokio::runtime::Handle::try_current().is_ok() {
598
780
return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone());
···
608
790
AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()),
609
791
AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()),
610
792
};
611
-
opts.headers.push((
612
-
CowStr::from("Authorization"),
613
-
CowStr::from(auth_value),
614
-
));
793
+
opts.headers
794
+
.push((CowStr::from("Authorization"), CowStr::from(auth_value)));
615
795
opts
616
796
}
617
797
+227
-24
crates/jacquard-oauth/src/dpop.rs
+227
-24
crates/jacquard-oauth/src/dpop.rs
···
109
109
)
110
110
.await
111
111
}
112
+
113
+
#[cfg(feature = "streaming")]
114
+
pub async fn send_streaming(
115
+
self,
116
+
request: Request<Vec<u8>>,
117
+
) -> Result<jacquard_common::xrpc::StreamingResponse>
118
+
where
119
+
C: jacquard_common::http_client::HttpClientExt,
120
+
{
121
+
wrap_request_with_dpop_streaming(
122
+
self.client,
123
+
self.data_source,
124
+
self.is_to_auth_server,
125
+
request,
126
+
)
127
+
.await
128
+
}
129
+
130
+
#[cfg(feature = "streaming")]
131
+
pub async fn send_bidirectional(
132
+
self,
133
+
parts: http::request::Parts,
134
+
body: jacquard_common::stream::ByteStream,
135
+
) -> Result<jacquard_common::xrpc::StreamingResponse>
136
+
where
137
+
C: jacquard_common::http_client::HttpClientExt,
138
+
{
139
+
wrap_request_with_dpop_bidirectional(
140
+
self.client,
141
+
self.data_source,
142
+
self.is_to_auth_server,
143
+
parts,
144
+
body,
145
+
)
146
+
.await
147
+
}
148
+
}
149
+
150
+
/// Extract authorization hash from request headers
151
+
fn extract_ath(headers: &http::HeaderMap) -> Option<CowStr<'static>> {
152
+
headers
153
+
.get("Authorization")
154
+
.filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP ")))
155
+
.map(|auth| {
156
+
URL_SAFE_NO_PAD
157
+
.encode(sha2::Sha256::digest(&auth.as_bytes()[5..]))
158
+
.into()
159
+
})
160
+
}
161
+
162
+
/// Get nonce from data source based on target
163
+
fn get_nonce<N: DpopDataSource>(data_source: &N, is_to_auth_server: bool) -> Option<CowStr<'_>> {
164
+
if is_to_auth_server {
165
+
data_source.authserver_nonce()
166
+
} else {
167
+
data_source.host_nonce()
168
+
}
169
+
}
170
+
171
+
/// Store nonce in data source based on target
172
+
fn store_nonce<N: DpopDataSource>(
173
+
data_source: &mut N,
174
+
is_to_auth_server: bool,
175
+
nonce: CowStr<'static>,
176
+
) {
177
+
if is_to_auth_server {
178
+
data_source.set_authserver_nonce(nonce);
179
+
} else {
180
+
data_source.set_host_nonce(nonce);
181
+
}
112
182
}
113
183
114
184
pub async fn wrap_request_with_dpop<T, N>(
···
124
194
let uri = request.uri().clone();
125
195
let method = request.method().to_cowstr().into_static();
126
196
let uri = uri.to_cowstr();
127
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
128
-
let ath = request
129
-
.headers()
130
-
.get("Authorization")
131
-
.filter(|v| v.to_str().is_ok_and(|s| s.starts_with("DPoP ")))
132
-
.map(|auth| {
133
-
URL_SAFE_NO_PAD
134
-
.encode(sha2::Sha256::digest(&auth.as_bytes()[5..]))
135
-
.into()
136
-
});
197
+
let ath = extract_ath(request.headers());
137
198
138
-
let init_nonce = if is_to_auth_server {
139
-
data_source.authserver_nonce()
140
-
} else {
141
-
data_source.host_nonce()
142
-
};
199
+
let init_nonce = get_nonce(data_source, is_to_auth_server);
143
200
let init_proof = build_dpop_proof(
144
201
data_source.key(),
145
202
method.clone(),
···
157
214
.headers()
158
215
.get("DPoP-Nonce")
159
216
.and_then(|v| v.to_str().ok())
160
-
.map(|c| c.to_cowstr());
217
+
.map(|c| CowStr::from(c.to_string()));
161
218
match &next_nonce {
162
219
Some(s) if next_nonce != init_nonce => {
163
-
// Store the fresh nonce for future requests
164
-
if is_to_auth_server {
165
-
data_source.set_authserver_nonce(s.clone());
166
-
} else {
167
-
data_source.set_host_nonce(s.clone());
168
-
}
220
+
store_nonce(data_source, is_to_auth_server, s.clone());
169
221
}
170
222
_ => {
171
-
// No nonce was returned or it is the same as the one we sent. No need to
172
-
// update the nonce store, or retry the request.
173
223
return Ok(response);
174
224
}
175
225
}
···
184
234
.await
185
235
.map_err(|e| Error::Inner(e.into()))?;
186
236
Ok(response)
237
+
}
238
+
239
+
#[cfg(feature = "streaming")]
240
+
pub async fn wrap_request_with_dpop_streaming<T, N>(
241
+
client: &T,
242
+
data_source: &mut N,
243
+
is_to_auth_server: bool,
244
+
mut request: Request<Vec<u8>>,
245
+
) -> Result<jacquard_common::xrpc::StreamingResponse>
246
+
where
247
+
T: jacquard_common::http_client::HttpClientExt,
248
+
N: DpopDataSource,
249
+
{
250
+
use jacquard_common::xrpc::StreamingResponse;
251
+
252
+
let uri = request.uri().clone();
253
+
let method = request.method().to_cowstr().into_static();
254
+
let uri = uri.to_cowstr();
255
+
let ath = extract_ath(request.headers());
256
+
257
+
let init_nonce = get_nonce(data_source, is_to_auth_server);
258
+
let init_proof = build_dpop_proof(
259
+
data_source.key(),
260
+
method.clone(),
261
+
uri.clone(),
262
+
init_nonce.clone(),
263
+
ath.clone(),
264
+
)?;
265
+
request.headers_mut().insert("DPoP", init_proof.parse()?);
266
+
let http_response = client
267
+
.send_http_streaming(request.clone())
268
+
.await
269
+
.map_err(|e| Error::Inner(e.into()))?;
270
+
271
+
let (parts, body) = http_response.into_parts();
272
+
let next_nonce = parts
273
+
.headers
274
+
.get("DPoP-Nonce")
275
+
.and_then(|v| v.to_str().ok())
276
+
.map(|c| CowStr::from(c.to_string()));
277
+
match &next_nonce {
278
+
Some(s) if next_nonce != init_nonce => {
279
+
store_nonce(data_source, is_to_auth_server, s.clone());
280
+
}
281
+
_ => {
282
+
return Ok(StreamingResponse::new(parts, body));
283
+
}
284
+
}
285
+
286
+
// For streaming responses, we can't easily check the body for use_dpop_nonce error
287
+
// We check status code + headers only
288
+
if !is_use_dpop_nonce_error_streaming(is_to_auth_server, parts.status, &parts.headers) {
289
+
return Ok(StreamingResponse::new(parts, body));
290
+
}
291
+
292
+
let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?;
293
+
request.headers_mut().insert("DPoP", next_proof.parse()?);
294
+
let http_response = client
295
+
.send_http_streaming(request)
296
+
.await
297
+
.map_err(|e| Error::Inner(e.into()))?;
298
+
let (parts, body) = http_response.into_parts();
299
+
Ok(StreamingResponse::new(parts, body))
300
+
}
301
+
302
+
#[cfg(feature = "streaming")]
303
+
pub async fn wrap_request_with_dpop_bidirectional<T, N>(
304
+
client: &T,
305
+
data_source: &mut N,
306
+
is_to_auth_server: bool,
307
+
mut parts: http::request::Parts,
308
+
body: jacquard_common::stream::ByteStream,
309
+
) -> Result<jacquard_common::xrpc::StreamingResponse>
310
+
where
311
+
T: jacquard_common::http_client::HttpClientExt,
312
+
N: DpopDataSource,
313
+
{
314
+
use jacquard_common::xrpc::StreamingResponse;
315
+
316
+
let uri = parts.uri.clone();
317
+
let method = parts.method.to_cowstr().into_static();
318
+
let uri = uri.to_cowstr();
319
+
let ath = extract_ath(&parts.headers);
320
+
321
+
let init_nonce = get_nonce(data_source, is_to_auth_server);
322
+
let init_proof = build_dpop_proof(
323
+
data_source.key(),
324
+
method.clone(),
325
+
uri.clone(),
326
+
init_nonce.clone(),
327
+
ath.clone(),
328
+
)?;
329
+
parts.headers.insert("DPoP", init_proof.parse()?);
330
+
331
+
// Clone the stream for potential retry
332
+
let (body1, body2) = body.tee();
333
+
334
+
let http_response = client
335
+
.send_http_bidirectional(parts.clone(), body1.into_inner())
336
+
.await
337
+
.map_err(|e| Error::Inner(e.into()))?;
338
+
339
+
let (resp_parts, resp_body) = http_response.into_parts();
340
+
let next_nonce = resp_parts
341
+
.headers
342
+
.get("DPoP-Nonce")
343
+
.and_then(|v| v.to_str().ok())
344
+
.map(|c| CowStr::from(c.to_string()));
345
+
match &next_nonce {
346
+
Some(s) if next_nonce != init_nonce => {
347
+
store_nonce(data_source, is_to_auth_server, s.clone());
348
+
}
349
+
_ => {
350
+
return Ok(StreamingResponse::new(resp_parts, resp_body));
351
+
}
352
+
}
353
+
354
+
// For streaming responses, we can't easily check the body for use_dpop_nonce error
355
+
// We check status code + headers only
356
+
if !is_use_dpop_nonce_error_streaming(is_to_auth_server, resp_parts.status, &resp_parts.headers)
357
+
{
358
+
return Ok(StreamingResponse::new(resp_parts, resp_body));
359
+
}
360
+
361
+
let next_proof = build_dpop_proof(data_source.key(), method, uri, next_nonce, ath)?;
362
+
parts.headers.insert("DPoP", next_proof.parse()?);
363
+
let http_response = client
364
+
.send_http_bidirectional(parts, body2.into_inner())
365
+
.await
366
+
.map_err(|e| Error::Inner(e.into()))?;
367
+
let (parts, body) = http_response.into_parts();
368
+
Ok(StreamingResponse::new(parts, body))
369
+
}
370
+
371
+
#[cfg(feature = "streaming")]
372
+
fn is_use_dpop_nonce_error_streaming(
373
+
is_to_auth_server: bool,
374
+
status: http::StatusCode,
375
+
headers: &http::HeaderMap,
376
+
) -> bool {
377
+
if is_to_auth_server && status == 400 {
378
+
// Can't check body for streaming, so we rely on DPoP-Nonce header presence
379
+
return false;
380
+
}
381
+
if !is_to_auth_server && status == 401 {
382
+
if let Some(www_auth) = headers
383
+
.get("WWW-Authenticate")
384
+
.and_then(|v| v.to_str().ok())
385
+
{
386
+
return www_auth.starts_with("DPoP") && www_auth.contains(r#"error="use_dpop_nonce""#);
387
+
}
388
+
}
389
+
false
187
390
}
188
391
189
392
#[inline]
+18
-2
crates/jacquard/Cargo.toml
+18
-2
crates/jacquard/Cargo.toml
···
12
12
license.workspace = true
13
13
14
14
[features]
15
-
default = ["api_full", "dns", "loopback", "derive"]
15
+
default = ["api_full", "dns", "loopback", "derive", "streaming"]
16
16
derive = ["dep:jacquard-derive"]
17
17
# Minimal API bindings
18
18
api = ["jacquard-api/minimal"]
···
38
38
"jacquard-identity/tracing",
39
39
]
40
40
dns = ["jacquard-identity/dns"]
41
-
streaming = ["jacquard-common/streaming"]
41
+
streaming = [
42
+
"jacquard-common/streaming",
43
+
"jacquard-oauth/streaming",
44
+
"jacquard-identity/streaming",
45
+
"dep:n0-future",
46
+
"dep:futures"
47
+
]
42
48
websocket = ["jacquard-common/websocket"]
43
49
44
50
[[example]]
···
74
80
path = "../../examples/read_tangled_repo.rs"
75
81
76
82
[[example]]
83
+
name = "stream_get_blob"
84
+
path = "../../examples/stream_get_blob.rs"
85
+
required-features = ["api_bluesky", "streaming"]
86
+
87
+
[[example]]
77
88
name = "resolve_did"
78
89
path = "../../examples/resolve_did.rs"
79
90
···
127
138
p256 = { workspace = true, features = ["ecdsa"] }
128
139
rand_core.workspace = true
129
140
tracing = { workspace = true, optional = true }
141
+
n0-future = { workspace = true, optional = true }
142
+
futures = { version = "0.3", optional = true }
130
143
131
144
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
132
145
reqwest = { workspace = true, features = [
···
142
155
[dev-dependencies]
143
156
clap.workspace = true
144
157
miette = { workspace = true, features = ["fancy"] }
158
+
viuer = { version = "0.9", features = ["print-file", "sixel"] }
159
+
tiff = { version = "0.6.0-alpha" }
160
+
image = { version = "0.25" }
145
161
146
162
[package.metadata.docs.rs]
147
163
features = ["api_all", "derive", "dns", "loopback"]
+151
-2
crates/jacquard/src/client.rs
+151
-2
crates/jacquard/src/client.rs
···
789
789
})?,
790
790
));
791
791
let response = self.send_with_opts(request, opts).await?;
792
+
let debug: serde_json::Value = serde_json::from_slice(response.buffer()).unwrap();
793
+
println!("json: {}", serde_json::to_string_pretty(&debug).unwrap());
792
794
let output = response.into_output().map_err(|e| match e {
793
795
XrpcError::Auth(auth) => AgentError::Auth(auth),
794
796
XrpcError::Generic(g) => AgentError::Generic(g),
···
912
914
}
913
915
}
914
916
917
+
#[cfg(feature = "streaming")]
918
+
impl<A> jacquard_common::http_client::HttpClientExt for Agent<A>
919
+
where
920
+
A: AgentSession + jacquard_common::http_client::HttpClientExt,
921
+
{
922
+
#[cfg(not(target_arch = "wasm32"))]
923
+
fn send_http_streaming(
924
+
&self,
925
+
request: http::Request<Vec<u8>>,
926
+
) -> impl Future<
927
+
Output = core::result::Result<
928
+
http::Response<jacquard_common::stream::ByteStream>,
929
+
Self::Error,
930
+
>,
931
+
> + Send {
932
+
self.inner.send_http_streaming(request)
933
+
}
934
+
935
+
#[cfg(target_arch = "wasm32")]
936
+
fn send_http_streaming(
937
+
&self,
938
+
request: http::Request<Vec<u8>>,
939
+
) -> impl Future<
940
+
Output = core::result::Result<
941
+
http::Response<jacquard_common::stream::ByteStream>,
942
+
Self::Error,
943
+
>,
944
+
> {
945
+
self.inner.send_http_streaming(request)
946
+
}
947
+
948
+
#[cfg(not(target_arch = "wasm32"))]
949
+
fn send_http_bidirectional<Str>(
950
+
&self,
951
+
parts: http::request::Parts,
952
+
body: Str,
953
+
) -> impl Future<
954
+
Output = core::result::Result<
955
+
http::Response<jacquard_common::stream::ByteStream>,
956
+
Self::Error,
957
+
>,
958
+
> + Send
959
+
where
960
+
Str: n0_future::Stream<
961
+
Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>,
962
+
> + Send
963
+
+ 'static,
964
+
{
965
+
self.inner.send_http_bidirectional(parts, body)
966
+
}
967
+
968
+
#[cfg(target_arch = "wasm32")]
969
+
fn send_http_bidirectional<Str>(
970
+
&self,
971
+
parts: http::request::Parts,
972
+
body: Str,
973
+
) -> impl Future<
974
+
Output = core::result::Result<
975
+
http::Response<jacquard_common::stream::ByteStream>,
976
+
Self::Error,
977
+
>,
978
+
>
979
+
where
980
+
Str: n0_future::Stream<
981
+
Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>,
982
+
> + 'static,
983
+
{
984
+
self.inner.send_http_bidirectional(parts, body)
985
+
}
986
+
}
987
+
915
988
impl<A: AgentSession> XrpcClient for Agent<A> {
916
-
fn base_uri(&self) -> url::Url {
917
-
self.inner.base_uri()
989
+
async fn base_uri(&self) -> url::Url {
990
+
self.inner.base_uri().await
918
991
}
919
992
fn opts(&self) -> impl Future<Output = CallOptions<'_>> {
920
993
self.inner.opts()
···
940
1013
<R as XrpcRequest>::Response: Send + Sync,
941
1014
{
942
1015
self.inner.send_with_opts(request, opts).await
1016
+
}
1017
+
}
1018
+
1019
+
#[cfg(feature = "streaming")]
1020
+
impl<A> jacquard_common::xrpc::XrpcStreamingClient for Agent<A>
1021
+
where
1022
+
A: AgentSession + jacquard_common::xrpc::XrpcStreamingClient,
1023
+
{
1024
+
#[cfg(not(target_arch = "wasm32"))]
1025
+
fn download<R>(
1026
+
&self,
1027
+
request: R,
1028
+
) -> impl Future<
1029
+
Output = core::result::Result<
1030
+
jacquard_common::xrpc::StreamingResponse,
1031
+
jacquard_common::StreamError,
1032
+
>,
1033
+
> + Send
1034
+
where
1035
+
R: XrpcRequest + Send + Sync,
1036
+
<R as XrpcRequest>::Response: Send + Sync,
1037
+
Self: Sync,
1038
+
{
1039
+
self.inner.download(request)
1040
+
}
1041
+
1042
+
#[cfg(target_arch = "wasm32")]
1043
+
fn download<R>(
1044
+
&self,
1045
+
request: R,
1046
+
) -> impl Future<
1047
+
Output = core::result::Result<
1048
+
jacquard_common::xrpc::StreamingResponse,
1049
+
jacquard_common::StreamError,
1050
+
>,
1051
+
>
1052
+
where
1053
+
R: XrpcRequest + Send + Sync,
1054
+
<R as XrpcRequest>::Response: Send + Sync,
1055
+
{
1056
+
self.inner.download(request)
1057
+
}
1058
+
1059
+
#[cfg(not(target_arch = "wasm32"))]
1060
+
fn stream<S>(
1061
+
&self,
1062
+
stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<'static>>,
1063
+
) -> impl Future<
1064
+
Output = core::result::Result<
1065
+
jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>,
1066
+
jacquard_common::StreamError,
1067
+
>,
1068
+
>
1069
+
where
1070
+
S: jacquard_common::xrpc::XrpcProcedureStream + 'static,
1071
+
<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp,
1072
+
Self: Sync,
1073
+
{
1074
+
self.inner.stream::<S>(stream)
1075
+
}
1076
+
1077
+
#[cfg(target_arch = "wasm32")]
1078
+
fn stream<S>(
1079
+
&self,
1080
+
stream: jacquard_common::xrpc::XrpcProcedureSend<S::Frame<'static>>,
1081
+
) -> impl Future<
1082
+
Output = core::result::Result<
1083
+
jacquard_common::xrpc::XrpcResponseStream<<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>>,
1084
+
jacquard_common::StreamError,
1085
+
>,
1086
+
>
1087
+
where
1088
+
S: jacquard_common::xrpc::XrpcProcedureStream + 'static,
1089
+
<<S as jacquard_common::xrpc::XrpcProcedureStream>::Response as jacquard_common::xrpc::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::XrpcStreamResp,
1090
+
{
1091
+
self.inner.stream::<S>(stream)
943
1092
}
944
1093
}
945
1094
+219
-28
crates/jacquard/src/client/credential_session.rs
+219
-28
crates/jacquard/src/client/credential_session.rs
···
433
433
T: HttpClient + XrpcExt + Send + Sync + 'static,
434
434
W: Send + Sync,
435
435
{
436
-
fn base_uri(&self) -> Url {
437
-
// base_uri is a synchronous trait method; avoid `.await` here.
438
-
// Under Tokio, use `block_in_place` to make a blocking RwLock read safe.
439
-
#[cfg(not(target_arch = "wasm32"))]
440
-
if tokio::runtime::Handle::try_current().is_ok() {
441
-
tokio::task::block_in_place(|| {
442
-
self.endpoint.blocking_read().clone().unwrap_or(
443
-
Url::parse("https://public.bsky.app")
444
-
.expect("public appview should be valid url"),
445
-
)
446
-
})
447
-
} else {
448
-
self.endpoint.blocking_read().clone().unwrap_or(
449
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
450
-
)
451
-
}
452
-
453
-
#[cfg(target_arch = "wasm32")]
454
-
{
455
-
self.endpoint.blocking_read().clone().unwrap_or(
456
-
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
457
-
)
458
-
}
436
+
async fn base_uri(&self) -> Url {
437
+
self.endpoint.read().await.clone().unwrap_or(
438
+
Url::parse("https://public.bsky.app").expect("public appview should be valid url"),
439
+
)
459
440
}
460
441
461
442
async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
···
476
457
R: XrpcRequest + Send + Sync,
477
458
<R as XrpcRequest>::Response: Send + Sync,
478
459
{
479
-
let base_uri = self.base_uri();
460
+
let base_uri = self.base_uri().await;
480
461
let auth = self.access_token().await;
481
462
opts.auth = auth;
482
463
let resp = self
···
512
493
}
513
494
}
514
495
496
+
#[cfg(feature = "streaming")]
497
+
impl<S, T, W> jacquard_common::http_client::HttpClientExt for CredentialSession<S, T, W>
498
+
where
499
+
S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static,
500
+
T: HttpClient + XrpcExt + jacquard_common::http_client::HttpClientExt + Send + Sync + 'static,
501
+
W: Send + Sync,
502
+
{
503
+
async fn send_http_streaming(
504
+
&self,
505
+
request: http::Request<Vec<u8>>,
506
+
) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error> {
507
+
self.client.send_http_streaming(request).await
508
+
}
509
+
510
+
async fn send_http_bidirectional<Str>(
511
+
&self,
512
+
parts: http::request::Parts,
513
+
body: Str,
514
+
) -> core::result::Result<http::Response<jacquard_common::stream::ByteStream>, Self::Error>
515
+
where
516
+
Str: n0_future::Stream<Item = core::result::Result<bytes::Bytes, jacquard_common::StreamError>>
517
+
+ Send
518
+
+ 'static,
519
+
{
520
+
self.client.send_http_bidirectional(parts, body).await
521
+
}
522
+
}
523
+
524
+
#[cfg(feature = "streaming")]
525
+
impl<S, T, W> jacquard_common::xrpc::XrpcStreamingClient for CredentialSession<S, T, W>
526
+
where
527
+
S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static,
528
+
T: HttpClient + XrpcExt + jacquard_common::http_client::HttpClientExt + Send + Sync + 'static,
529
+
W: Send + Sync,
530
+
{
531
+
async fn download<R>(
532
+
&self,
533
+
request: R,
534
+
) -> core::result::Result<jacquard_common::xrpc::StreamingResponse, jacquard_common::StreamError>
535
+
where
536
+
R: XrpcRequest + Send + Sync,
537
+
<R as XrpcRequest>::Response: Send + Sync,
538
+
{
539
+
use jacquard_common::{StreamError, xrpc::build_http_request};
540
+
541
+
let base_uri = <Self as XrpcClient>::base_uri(self).await;
542
+
let mut opts = self.options.read().await.clone();
543
+
opts.auth = self.access_token().await;
544
+
545
+
let http_request = build_http_request(&base_uri, &request, &opts)
546
+
.map_err(|e| StreamError::protocol(e.to_string()))?;
547
+
548
+
let response = self
549
+
.client
550
+
.send_http_streaming(http_request.clone())
551
+
.await
552
+
.map_err(StreamError::transport)?;
553
+
554
+
let (parts, body) = response.into_parts();
555
+
let status = parts.status;
556
+
557
+
// Check if expired based on status code
558
+
if status == http::StatusCode::UNAUTHORIZED || status == http::StatusCode::BAD_REQUEST {
559
+
// Try to refresh
560
+
let auth = self.refresh().await.map_err(StreamError::transport)?;
561
+
opts.auth = Some(auth);
562
+
563
+
let http_request = build_http_request(&base_uri, &request, &opts)
564
+
.map_err(|e| StreamError::protocol(e.to_string()))?;
565
+
566
+
let response = self
567
+
.client
568
+
.send_http_streaming(http_request)
569
+
.await
570
+
.map_err(StreamError::transport)?;
571
+
let (parts, body) = response.into_parts();
572
+
Ok(jacquard_common::xrpc::StreamingResponse::new(parts, body))
573
+
} else {
574
+
Ok(jacquard_common::xrpc::StreamingResponse::new(parts, body))
575
+
}
576
+
}
577
+
578
+
async fn stream<Str>(
579
+
&self,
580
+
stream: jacquard_common::xrpc::streaming::XrpcProcedureSend<Str::Frame<'static>>,
581
+
) -> core::result::Result<
582
+
jacquard_common::xrpc::streaming::XrpcResponseStream<
583
+
<<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>,
584
+
>,
585
+
jacquard_common::StreamError,
586
+
>
587
+
where
588
+
Str: jacquard_common::xrpc::streaming::XrpcProcedureStream + 'static,
589
+
<<Str as jacquard_common::xrpc::streaming::XrpcProcedureStream>::Response as jacquard_common::xrpc::streaming::XrpcStreamResp>::Frame<'static>: jacquard_common::xrpc::streaming::XrpcStreamResp,
590
+
{
591
+
use jacquard_common::StreamError;
592
+
use n0_future::{StreamExt, TryStreamExt};
593
+
594
+
let base_uri = self.base_uri().await;
595
+
let mut opts = self.options.read().await.clone();
596
+
opts.auth = self.access_token().await;
597
+
598
+
let mut url = base_uri;
599
+
let mut path = url.path().trim_end_matches('/').to_owned();
600
+
path.push_str("/xrpc/");
601
+
path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID);
602
+
url.set_path(&path);
603
+
604
+
let mut builder = http::Request::post(url.to_string());
605
+
606
+
if let Some(token) = &opts.auth {
607
+
use jacquard_common::AuthorizationToken;
608
+
let hv = match token {
609
+
AuthorizationToken::Bearer(t) => {
610
+
http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref()))
611
+
}
612
+
AuthorizationToken::Dpop(t) => {
613
+
http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref()))
614
+
}
615
+
}
616
+
.map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?;
617
+
builder = builder.header(http::header::AUTHORIZATION, hv);
618
+
}
619
+
620
+
if let Some(proxy) = &opts.atproto_proxy {
621
+
builder = builder.header("atproto-proxy", proxy.as_ref());
622
+
}
623
+
if let Some(labelers) = &opts.atproto_accept_labelers {
624
+
if !labelers.is_empty() {
625
+
let joined = labelers
626
+
.iter()
627
+
.map(|s| s.as_ref())
628
+
.collect::<Vec<_>>()
629
+
.join(", ");
630
+
builder = builder.header("atproto-accept-labelers", joined);
631
+
}
632
+
}
633
+
for (name, value) in &opts.extra_headers {
634
+
builder = builder.header(name, value);
635
+
}
636
+
637
+
let (parts, _) = builder
638
+
.body(())
639
+
.map_err(|e| StreamError::protocol(e.to_string()))?
640
+
.into_parts();
641
+
642
+
let body_stream =
643
+
jacquard_common::stream::ByteStream::new(stream.0.map_ok(|f| f.buffer).boxed());
644
+
645
+
let response = self
646
+
.client
647
+
.send_http_bidirectional(parts.clone(), body_stream.into_inner())
648
+
.await
649
+
.map_err(StreamError::transport)?;
650
+
651
+
let (resp_parts, resp_body) = response.into_parts();
652
+
let status = resp_parts.status;
653
+
654
+
// Check if expired
655
+
if status == http::StatusCode::UNAUTHORIZED || status == http::StatusCode::BAD_REQUEST {
656
+
// Try to refresh
657
+
let auth = self.refresh().await.map_err(StreamError::transport)?;
658
+
opts.auth = Some(auth);
659
+
660
+
// Rebuild request with new auth
661
+
let mut builder = http::Request::post(url.to_string());
662
+
if let Some(token) = &opts.auth {
663
+
use jacquard_common::AuthorizationToken;
664
+
let hv = match token {
665
+
AuthorizationToken::Bearer(t) => {
666
+
http::HeaderValue::from_str(&format!("Bearer {}", t.as_ref()))
667
+
}
668
+
AuthorizationToken::Dpop(t) => {
669
+
http::HeaderValue::from_str(&format!("DPoP {}", t.as_ref()))
670
+
}
671
+
}
672
+
.map_err(|e| StreamError::protocol(format!("Invalid authorization token: {}", e)))?;
673
+
builder = builder.header(http::header::AUTHORIZATION, hv);
674
+
}
675
+
if let Some(proxy) = &opts.atproto_proxy {
676
+
builder = builder.header("atproto-proxy", proxy.as_ref());
677
+
}
678
+
if let Some(labelers) = &opts.atproto_accept_labelers {
679
+
if !labelers.is_empty() {
680
+
let joined = labelers
681
+
.iter()
682
+
.map(|s| s.as_ref())
683
+
.collect::<Vec<_>>()
684
+
.join(", ");
685
+
builder = builder.header("atproto-accept-labelers", joined);
686
+
}
687
+
}
688
+
for (name, value) in &opts.extra_headers {
689
+
builder = builder.header(name, value);
690
+
}
691
+
692
+
let (parts, _) = builder
693
+
.body(())
694
+
.map_err(|e| StreamError::protocol(e.to_string()))?
695
+
.into_parts();
696
+
697
+
// Can't retry with the same stream - it's been consumed
698
+
// This is a limitation of streaming upload with auth refresh
699
+
return Err(StreamError::protocol("Authentication failed on streaming upload and stream cannot be retried".to_string()));
700
+
}
701
+
702
+
Ok(jacquard_common::xrpc::streaming::XrpcResponseStream::from_typed_parts(
703
+
resp_parts, resp_body,
704
+
))
705
+
}
706
+
}
707
+
515
708
impl<S, T, W> IdentityResolver for CredentialSession<S, T, W>
516
709
where
517
710
S: SessionStore<SessionKey, AtpSession> + Send + Sync + 'static,
···
596
789
AuthorizationToken::Bearer(t) => format!("Bearer {}", t.as_ref()),
597
790
AuthorizationToken::Dpop(t) => format!("DPoP {}", t.as_ref()),
598
791
};
599
-
opts.headers.push((
600
-
CowStr::from("Authorization"),
601
-
CowStr::from(auth_value),
602
-
));
792
+
opts.headers
793
+
.push((CowStr::from("Authorization"), CowStr::from(auth_value)));
603
794
}
604
795
opts
605
796
}
+3
crates/jacquard/src/lib.rs
+3
crates/jacquard/src/lib.rs
+61
crates/jacquard/src/streaming/blob.rs
+61
crates/jacquard/src/streaming/blob.rs
···
1
+
//! Streaming support for blob uploads
2
+
3
+
use bytes::Bytes;
4
+
use jacquard_api::com_atproto::repo::upload_blob::{UploadBlob, UploadBlobOutput};
5
+
use jacquard_common::{
6
+
StreamError,
7
+
xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp},
8
+
};
9
+
use serde::{Deserialize, Serialize};
10
+
11
+
/// Streaming implementation for com.atproto.repo.uploadBlob
12
+
pub struct UploadBlobStream;
13
+
14
+
impl XrpcProcedureStream for UploadBlobStream {
15
+
const NSID: &'static str = "com.atproto.repo.uploadBlob";
16
+
const ENCODING: &'static str = "*/*";
17
+
18
+
type Frame<'de> = Bytes;
19
+
type Request = UploadBlob;
20
+
type Response = UploadBlobStreamResponse;
21
+
22
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
23
+
where
24
+
Self::Frame<'de>: Serialize,
25
+
{
26
+
Ok(data)
27
+
}
28
+
29
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
30
+
where
31
+
Self::Frame<'de>: Deserialize<'de>,
32
+
{
33
+
Ok(Bytes::copy_from_slice(frame))
34
+
}
35
+
}
36
+
37
+
/// Response marker for streaming uploadBlob
38
+
pub struct UploadBlobStreamResponse;
39
+
40
+
impl XrpcStreamResp for UploadBlobStreamResponse {
41
+
const NSID: &'static str = "com.atproto.repo.uploadBlob";
42
+
const ENCODING: &'static str = "application/json";
43
+
44
+
type Frame<'de> = UploadBlobOutput<'de>;
45
+
46
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
47
+
where
48
+
Self::Frame<'de>: Serialize,
49
+
{
50
+
Ok(Bytes::from_owner(
51
+
serde_json::to_vec(&data).map_err(StreamError::encode)?,
52
+
))
53
+
}
54
+
55
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
56
+
where
57
+
Self::Frame<'de>: Deserialize<'de>,
58
+
{
59
+
Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?)
60
+
}
61
+
}
+83
crates/jacquard/src/streaming/repo.rs
+83
crates/jacquard/src/streaming/repo.rs
···
1
+
//! Streaming support for repository operations
2
+
3
+
use bytes::Bytes;
4
+
use jacquard_api::com_atproto::repo::import_repo::ImportRepo;
5
+
use jacquard_common::{
6
+
xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp},
7
+
StreamError,
8
+
};
9
+
use serde::{Deserialize, Serialize};
10
+
11
+
/// Streaming implementation for com.atproto.repo.importRepo
12
+
pub struct ImportRepoStream;
13
+
14
+
impl XrpcProcedureStream for ImportRepoStream {
15
+
const NSID: &'static str = "com.atproto.repo.importRepo";
16
+
const ENCODING: &'static str = "application/vnd.ipld.car";
17
+
18
+
type Frame<'de> = Bytes;
19
+
type Request = ImportRepo;
20
+
type Response = ImportRepoStreamResponse;
21
+
22
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
23
+
where
24
+
Self::Frame<'de>: Serialize,
25
+
{
26
+
Ok(data)
27
+
}
28
+
29
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
30
+
where
31
+
Self::Frame<'de>: Deserialize<'de>,
32
+
{
33
+
Ok(Bytes::copy_from_slice(frame))
34
+
}
35
+
}
36
+
37
+
/// Response marker for streaming importRepo
38
+
pub struct ImportRepoStreamResponse;
39
+
40
+
impl XrpcStreamResp for ImportRepoStreamResponse {
41
+
const NSID: &'static str = "com.atproto.repo.importRepo";
42
+
const ENCODING: &'static str = "application/json";
43
+
44
+
type Frame<'de> = ();
45
+
46
+
fn encode_frame<'de>(_data: Self::Frame<'de>) -> Result<Bytes, StreamError>
47
+
where
48
+
Self::Frame<'de>: Serialize,
49
+
{
50
+
Ok(Bytes::new())
51
+
}
52
+
53
+
fn decode_frame<'de>(_frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
54
+
where
55
+
Self::Frame<'de>: Deserialize<'de>,
56
+
{
57
+
Ok(())
58
+
}
59
+
}
60
+
61
+
/// Streaming implementation for com.atproto.sync.getRepo
62
+
pub struct GetRepoStream;
63
+
64
+
impl XrpcStreamResp for GetRepoStream {
65
+
const NSID: &'static str = "com.atproto.sync.getRepo";
66
+
const ENCODING: &'static str = "application/vnd.ipld.car";
67
+
68
+
type Frame<'de> = Bytes;
69
+
70
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
71
+
where
72
+
Self::Frame<'de>: Serialize,
73
+
{
74
+
Ok(data)
75
+
}
76
+
77
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
78
+
where
79
+
Self::Frame<'de>: Deserialize<'de>,
80
+
{
81
+
Ok(Bytes::copy_from_slice(frame))
82
+
}
83
+
}
+61
crates/jacquard/src/streaming/video.rs
+61
crates/jacquard/src/streaming/video.rs
···
1
+
//! Streaming support for video uploads
2
+
3
+
use bytes::Bytes;
4
+
use jacquard_api::app_bsky::video::upload_video::{UploadVideo, UploadVideoOutput};
5
+
use jacquard_common::{
6
+
xrpc::streaming::{XrpcProcedureStream, XrpcStreamResp},
7
+
StreamError,
8
+
};
9
+
use serde::{Deserialize, Serialize};
10
+
11
+
/// Streaming implementation for app.bsky.video.uploadVideo
12
+
pub struct UploadVideoStream;
13
+
14
+
impl XrpcProcedureStream for UploadVideoStream {
15
+
const NSID: &'static str = "app.bsky.video.uploadVideo";
16
+
const ENCODING: &'static str = "video/mp4";
17
+
18
+
type Frame<'de> = Bytes;
19
+
type Request = UploadVideo;
20
+
type Response = UploadVideoStreamResponse;
21
+
22
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
23
+
where
24
+
Self::Frame<'de>: Serialize,
25
+
{
26
+
Ok(data)
27
+
}
28
+
29
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
30
+
where
31
+
Self::Frame<'de>: Deserialize<'de>,
32
+
{
33
+
Ok(Bytes::copy_from_slice(frame))
34
+
}
35
+
}
36
+
37
+
/// Response marker for streaming uploadVideo
38
+
pub struct UploadVideoStreamResponse;
39
+
40
+
impl XrpcStreamResp for UploadVideoStreamResponse {
41
+
const NSID: &'static str = "app.bsky.video.uploadVideo";
42
+
const ENCODING: &'static str = "application/json";
43
+
44
+
type Frame<'de> = UploadVideoOutput<'de>;
45
+
46
+
fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
47
+
where
48
+
Self::Frame<'de>: Serialize,
49
+
{
50
+
Ok(Bytes::from_owner(
51
+
serde_json::to_vec(&data).map_err(StreamError::encode)?,
52
+
))
53
+
}
54
+
55
+
fn decode_frame<'de>(frame: &'de [u8]) -> Result<Self::Frame<'de>, StreamError>
56
+
where
57
+
Self::Frame<'de>: Deserialize<'de>,
58
+
{
59
+
Ok(serde_json::from_slice(frame).map_err(StreamError::decode)?)
60
+
}
61
+
}
+61
examples/stream_get_blob.rs
+61
examples/stream_get_blob.rs
···
1
+
use clap::Parser;
2
+
use jacquard::StreamingResponse;
3
+
use jacquard::api::com_atproto::sync::get_blob::GetBlob;
4
+
use jacquard::client::Agent;
5
+
use jacquard::types::cid::Cid;
6
+
use jacquard::types::did::Did;
7
+
use jacquard::xrpc::XrpcStreamingClient;
8
+
use jacquard_oauth::authstore::MemoryAuthStore;
9
+
use jacquard_oauth::client::OAuthClient;
10
+
use jacquard_oauth::loopback::LoopbackConfig;
11
+
use n0_future::StreamExt;
12
+
13
+
#[derive(Parser, Debug)]
14
+
#[command(
15
+
author,
16
+
version,
17
+
about = "Download a blob from a PDS and stream the response, then display it, if it's an image"
18
+
)]
19
+
struct Args {
20
+
input: String,
21
+
#[arg(short, long)]
22
+
did: String,
23
+
#[arg(short, long)]
24
+
cid: String,
25
+
}
26
+
27
+
#[tokio::main]
28
+
async fn main() -> miette::Result<()> {
29
+
let args = Args::parse();
30
+
31
+
let oauth = OAuthClient::with_default_config(MemoryAuthStore::new());
32
+
let session = oauth
33
+
.login_with_local_server(args.input, Default::default(), LoopbackConfig::default())
34
+
.await?;
35
+
36
+
let agent: Agent<_> = Agent::from(session);
37
+
// Use the streaming `.download()` method with the generated API parameter struct
38
+
let output: StreamingResponse = agent
39
+
.download(GetBlob {
40
+
did: Did::new_owned(args.did)?,
41
+
cid: Cid::str(&args.cid),
42
+
})
43
+
.await?;
44
+
45
+
let (parts, body_stream) = output.into_parts();
46
+
47
+
println!("Parts: {:?}", parts);
48
+
49
+
let mut buf: Vec<u8> = Vec::new();
50
+
let mut stream = body_stream.into_inner();
51
+
52
+
while let Some(Ok(chunk)) = stream.as_mut().next().await {
53
+
buf.append(&mut chunk.to_vec());
54
+
}
55
+
56
+
if let Ok(img) = image::load_from_memory(&buf) {
57
+
viuer::print(&img, &viuer::Config::default()).expect("Image printing failed.");
58
+
}
59
+
60
+
Ok(())
61
+
}