+2263
-405
Cargo.lock
+2263
-405
Cargo.lock
···
39
39
]
40
40
41
41
[[package]]
42
+
name = "aligned-vec"
43
+
version = "0.5.0"
44
+
source = "registry+https://github.com/rust-lang/crates.io-index"
45
+
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
46
+
47
+
[[package]]
42
48
name = "allocator-api2"
43
49
version = "0.2.21"
44
50
source = "registry+https://github.com/rust-lang/crates.io-index"
···
116
122
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
117
123
118
124
[[package]]
125
+
name = "arbitrary"
126
+
version = "1.4.1"
127
+
source = "registry+https://github.com/rust-lang/crates.io-index"
128
+
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
129
+
130
+
[[package]]
131
+
name = "arg_enum_proc_macro"
132
+
version = "0.3.4"
133
+
source = "registry+https://github.com/rust-lang/crates.io-index"
134
+
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
135
+
dependencies = [
136
+
"proc-macro2",
137
+
"quote",
138
+
"syn 2.0.101",
139
+
]
140
+
141
+
[[package]]
119
142
name = "argon2"
120
143
version = "0.5.3"
121
144
source = "registry+https://github.com/rust-lang/crates.io-index"
···
138
161
version = "0.7.6"
139
162
source = "registry+https://github.com/rust-lang/crates.io-index"
140
163
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
164
+
165
+
[[package]]
166
+
name = "ascii_utils"
167
+
version = "0.9.3"
168
+
source = "registry+https://github.com/rust-lang/crates.io-index"
169
+
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
141
170
142
171
[[package]]
143
172
name = "async-channel"
···
176
205
]
177
206
178
207
[[package]]
208
+
name = "async-event-emitter"
209
+
version = "0.1.4"
210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
211
+
checksum = "0780980d45c26a07355c8eef957978ce91fb1842b98b0d194e6e5808f428e52a"
212
+
dependencies = [
213
+
"anyhow",
214
+
"bincode",
215
+
"dashmap",
216
+
"futures",
217
+
"lazy_static",
218
+
"serde",
219
+
"uuid 1.16.0",
220
+
]
221
+
222
+
[[package]]
179
223
name = "async-io"
180
224
version = "2.4.0"
181
225
source = "registry+https://github.com/rust-lang/crates.io-index"
···
293
337
]
294
338
295
339
[[package]]
296
-
name = "atoi"
297
-
version = "2.0.0"
340
+
name = "atomic"
341
+
version = "0.5.3"
298
342
source = "registry+https://github.com/rust-lang/crates.io-index"
299
-
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
300
-
dependencies = [
301
-
"num-traits",
302
-
]
343
+
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
303
344
304
345
[[package]]
305
346
name = "atomic"
···
318
359
319
360
[[package]]
320
361
name = "atrium-api"
362
+
version = "0.24.10"
363
+
source = "registry+https://github.com/rust-lang/crates.io-index"
364
+
checksum = "9c5d74937642f6b21814e82d80f54d55ebd985b681bffbe27c8a76e726c3c4db"
365
+
dependencies = [
366
+
"atrium-xrpc",
367
+
"chrono",
368
+
"http 1.3.1",
369
+
"ipld-core",
370
+
"langtag",
371
+
"regex",
372
+
"serde",
373
+
"serde_bytes",
374
+
"serde_json",
375
+
"thiserror 1.0.69",
376
+
"tokio",
377
+
"trait-variant",
378
+
]
379
+
380
+
[[package]]
381
+
name = "atrium-api"
321
382
version = "0.25.3"
322
383
source = "registry+https://github.com/rust-lang/crates.io-index"
323
384
checksum = "7225f0ca3c78564b784828e3db3e92619cf6e786530c3468df73f49deebc0bd4"
···
325
386
"atrium-common",
326
387
"atrium-xrpc",
327
388
"chrono",
328
-
"http",
389
+
"http 1.3.1",
329
390
"ipld-core",
330
391
"langtag",
331
392
"regex",
···
358
419
source = "registry+https://github.com/rust-lang/crates.io-index"
359
420
checksum = "73a3da430c71dd9006d61072c20771f264e5c498420a49c32305ceab8bd71955"
360
421
dependencies = [
361
-
"ecdsa",
422
+
"ecdsa 0.16.9",
362
423
"k256",
363
424
"multibase",
364
-
"p256",
425
+
"p256 0.13.2",
365
426
"thiserror 1.0.69",
366
427
]
367
428
···
372
433
checksum = "1ebe74e137537277a290faab14154976851c7ff3cfc07f9872b1eb127a6a6ea3"
373
434
dependencies = [
374
435
"async-stream",
375
-
"atrium-api",
436
+
"atrium-api 0.25.3",
376
437
"futures",
377
438
"ipld-core",
378
439
"serde",
···
391
452
source = "registry+https://github.com/rust-lang/crates.io-index"
392
453
checksum = "0216ad50ce34e9ff982e171c3659e65dedaa2ed5ac2994524debdc9a9647ffa8"
393
454
dependencies = [
394
-
"http",
455
+
"http 1.3.1",
395
456
"serde",
396
457
"serde_html_form",
397
458
"serde_json",
···
406
467
checksum = "e099e5171f79faef52364ef0657a4cab086a71b384a779a29597a91b780de0d5"
407
468
dependencies = [
408
469
"atrium-xrpc",
409
-
"reqwest",
470
+
"reqwest 0.12.15",
410
471
]
411
472
412
473
[[package]]
···
416
477
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
417
478
418
479
[[package]]
480
+
name = "av1-grain"
481
+
version = "0.2.3"
482
+
source = "registry+https://github.com/rust-lang/crates.io-index"
483
+
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
484
+
dependencies = [
485
+
"anyhow",
486
+
"arrayvec",
487
+
"log",
488
+
"nom",
489
+
"num-rational",
490
+
"v_frame",
491
+
]
492
+
493
+
[[package]]
494
+
name = "avif-serialize"
495
+
version = "0.8.3"
496
+
source = "registry+https://github.com/rust-lang/crates.io-index"
497
+
checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
498
+
dependencies = [
499
+
"arrayvec",
500
+
]
501
+
502
+
[[package]]
503
+
name = "aws-config"
504
+
version = "1.6.2"
505
+
source = "registry+https://github.com/rust-lang/crates.io-index"
506
+
checksum = "b6fcc63c9860579e4cb396239570e979376e70aab79e496621748a09913f8b36"
507
+
dependencies = [
508
+
"aws-credential-types",
509
+
"aws-runtime",
510
+
"aws-sdk-sso",
511
+
"aws-sdk-ssooidc",
512
+
"aws-sdk-sts",
513
+
"aws-smithy-async",
514
+
"aws-smithy-http",
515
+
"aws-smithy-json",
516
+
"aws-smithy-runtime",
517
+
"aws-smithy-runtime-api",
518
+
"aws-smithy-types",
519
+
"aws-types",
520
+
"bytes",
521
+
"fastrand 2.3.0",
522
+
"hex",
523
+
"http 1.3.1",
524
+
"ring",
525
+
"time",
526
+
"tokio",
527
+
"tracing",
528
+
"url",
529
+
"zeroize",
530
+
]
531
+
532
+
[[package]]
533
+
name = "aws-credential-types"
534
+
version = "1.2.3"
535
+
source = "registry+https://github.com/rust-lang/crates.io-index"
536
+
checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465"
537
+
dependencies = [
538
+
"aws-smithy-async",
539
+
"aws-smithy-runtime-api",
540
+
"aws-smithy-types",
541
+
"zeroize",
542
+
]
543
+
544
+
[[package]]
419
545
name = "aws-lc-rs"
420
546
version = "1.13.0"
421
547
source = "registry+https://github.com/rust-lang/crates.io-index"
···
439
565
]
440
566
441
567
[[package]]
568
+
name = "aws-runtime"
569
+
version = "1.5.7"
570
+
source = "registry+https://github.com/rust-lang/crates.io-index"
571
+
checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c"
572
+
dependencies = [
573
+
"aws-credential-types",
574
+
"aws-sigv4",
575
+
"aws-smithy-async",
576
+
"aws-smithy-eventstream",
577
+
"aws-smithy-http",
578
+
"aws-smithy-runtime",
579
+
"aws-smithy-runtime-api",
580
+
"aws-smithy-types",
581
+
"aws-types",
582
+
"bytes",
583
+
"fastrand 2.3.0",
584
+
"http 0.2.12",
585
+
"http-body 0.4.6",
586
+
"percent-encoding",
587
+
"pin-project-lite",
588
+
"tracing",
589
+
"uuid 1.16.0",
590
+
]
591
+
592
+
[[package]]
593
+
name = "aws-sdk-s3"
594
+
version = "1.85.0"
595
+
source = "registry+https://github.com/rust-lang/crates.io-index"
596
+
checksum = "d5c82dae9304e7ced2ff6cca43dceb2d6de534c95a506ff0f168a7463c9a885d"
597
+
dependencies = [
598
+
"aws-credential-types",
599
+
"aws-runtime",
600
+
"aws-sigv4",
601
+
"aws-smithy-async",
602
+
"aws-smithy-checksums",
603
+
"aws-smithy-eventstream",
604
+
"aws-smithy-http",
605
+
"aws-smithy-json",
606
+
"aws-smithy-runtime",
607
+
"aws-smithy-runtime-api",
608
+
"aws-smithy-types",
609
+
"aws-smithy-xml",
610
+
"aws-types",
611
+
"bytes",
612
+
"fastrand 2.3.0",
613
+
"hex",
614
+
"hmac",
615
+
"http 0.2.12",
616
+
"http 1.3.1",
617
+
"http-body 0.4.6",
618
+
"lru",
619
+
"once_cell",
620
+
"percent-encoding",
621
+
"regex-lite",
622
+
"sha2",
623
+
"tracing",
624
+
"url",
625
+
]
626
+
627
+
[[package]]
628
+
name = "aws-sdk-sso"
629
+
version = "1.67.0"
630
+
source = "registry+https://github.com/rust-lang/crates.io-index"
631
+
checksum = "0d4863da26489d1e6da91d7e12b10c17e86c14f94c53f416bd10e0a9c34057ba"
632
+
dependencies = [
633
+
"aws-credential-types",
634
+
"aws-runtime",
635
+
"aws-smithy-async",
636
+
"aws-smithy-http",
637
+
"aws-smithy-json",
638
+
"aws-smithy-runtime",
639
+
"aws-smithy-runtime-api",
640
+
"aws-smithy-types",
641
+
"aws-types",
642
+
"bytes",
643
+
"fastrand 2.3.0",
644
+
"http 0.2.12",
645
+
"once_cell",
646
+
"regex-lite",
647
+
"tracing",
648
+
]
649
+
650
+
[[package]]
651
+
name = "aws-sdk-ssooidc"
652
+
version = "1.68.0"
653
+
source = "registry+https://github.com/rust-lang/crates.io-index"
654
+
checksum = "95caa3998d7237789b57b95a8e031f60537adab21fa84c91e35bef9455c652e4"
655
+
dependencies = [
656
+
"aws-credential-types",
657
+
"aws-runtime",
658
+
"aws-smithy-async",
659
+
"aws-smithy-http",
660
+
"aws-smithy-json",
661
+
"aws-smithy-runtime",
662
+
"aws-smithy-runtime-api",
663
+
"aws-smithy-types",
664
+
"aws-types",
665
+
"bytes",
666
+
"fastrand 2.3.0",
667
+
"http 0.2.12",
668
+
"once_cell",
669
+
"regex-lite",
670
+
"tracing",
671
+
]
672
+
673
+
[[package]]
674
+
name = "aws-sdk-sts"
675
+
version = "1.68.0"
676
+
source = "registry+https://github.com/rust-lang/crates.io-index"
677
+
checksum = "4939f6f449a37308a78c5a910fd91265479bd2bb11d186f0b8fc114d89ec828d"
678
+
dependencies = [
679
+
"aws-credential-types",
680
+
"aws-runtime",
681
+
"aws-smithy-async",
682
+
"aws-smithy-http",
683
+
"aws-smithy-json",
684
+
"aws-smithy-query",
685
+
"aws-smithy-runtime",
686
+
"aws-smithy-runtime-api",
687
+
"aws-smithy-types",
688
+
"aws-smithy-xml",
689
+
"aws-types",
690
+
"fastrand 2.3.0",
691
+
"http 0.2.12",
692
+
"once_cell",
693
+
"regex-lite",
694
+
"tracing",
695
+
]
696
+
697
+
[[package]]
698
+
name = "aws-sigv4"
699
+
version = "1.3.1"
700
+
source = "registry+https://github.com/rust-lang/crates.io-index"
701
+
checksum = "3503af839bd8751d0bdc5a46b9cac93a003a353e635b0c12cf2376b5b53e41ea"
702
+
dependencies = [
703
+
"aws-credential-types",
704
+
"aws-smithy-eventstream",
705
+
"aws-smithy-http",
706
+
"aws-smithy-runtime-api",
707
+
"aws-smithy-types",
708
+
"bytes",
709
+
"crypto-bigint 0.5.5",
710
+
"form_urlencoded",
711
+
"hex",
712
+
"hmac",
713
+
"http 0.2.12",
714
+
"http 1.3.1",
715
+
"p256 0.11.1",
716
+
"percent-encoding",
717
+
"ring",
718
+
"sha2",
719
+
"subtle",
720
+
"time",
721
+
"tracing",
722
+
"zeroize",
723
+
]
724
+
725
+
[[package]]
726
+
name = "aws-smithy-async"
727
+
version = "1.2.5"
728
+
source = "registry+https://github.com/rust-lang/crates.io-index"
729
+
checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c"
730
+
dependencies = [
731
+
"futures-util",
732
+
"pin-project-lite",
733
+
"tokio",
734
+
]
735
+
736
+
[[package]]
737
+
name = "aws-smithy-checksums"
738
+
version = "0.63.1"
739
+
source = "registry+https://github.com/rust-lang/crates.io-index"
740
+
checksum = "b65d21e1ba6f2cdec92044f904356a19f5ad86961acf015741106cdfafd747c0"
741
+
dependencies = [
742
+
"aws-smithy-http",
743
+
"aws-smithy-types",
744
+
"bytes",
745
+
"crc32c",
746
+
"crc32fast",
747
+
"crc64fast-nvme",
748
+
"hex",
749
+
"http 0.2.12",
750
+
"http-body 0.4.6",
751
+
"md-5",
752
+
"pin-project-lite",
753
+
"sha1",
754
+
"sha2",
755
+
"tracing",
756
+
]
757
+
758
+
[[package]]
759
+
name = "aws-smithy-eventstream"
760
+
version = "0.60.8"
761
+
source = "registry+https://github.com/rust-lang/crates.io-index"
762
+
checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a"
763
+
dependencies = [
764
+
"aws-smithy-types",
765
+
"bytes",
766
+
"crc32fast",
767
+
]
768
+
769
+
[[package]]
770
+
name = "aws-smithy-http"
771
+
version = "0.62.1"
772
+
source = "registry+https://github.com/rust-lang/crates.io-index"
773
+
checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9"
774
+
dependencies = [
775
+
"aws-smithy-eventstream",
776
+
"aws-smithy-runtime-api",
777
+
"aws-smithy-types",
778
+
"bytes",
779
+
"bytes-utils",
780
+
"futures-core",
781
+
"http 0.2.12",
782
+
"http 1.3.1",
783
+
"http-body 0.4.6",
784
+
"percent-encoding",
785
+
"pin-project-lite",
786
+
"pin-utils",
787
+
"tracing",
788
+
]
789
+
790
+
[[package]]
791
+
name = "aws-smithy-http-client"
792
+
version = "1.0.2"
793
+
source = "registry+https://github.com/rust-lang/crates.io-index"
794
+
checksum = "7e44697a9bded898dcd0b1cb997430d949b87f4f8940d91023ae9062bf218250"
795
+
dependencies = [
796
+
"aws-smithy-async",
797
+
"aws-smithy-runtime-api",
798
+
"aws-smithy-types",
799
+
"h2 0.4.9",
800
+
"http 0.2.12",
801
+
"http 1.3.1",
802
+
"http-body 0.4.6",
803
+
"hyper 0.14.32",
804
+
"hyper 1.6.0",
805
+
"hyper-rustls 0.24.2",
806
+
"hyper-rustls 0.27.5",
807
+
"hyper-util",
808
+
"pin-project-lite",
809
+
"rustls 0.21.12",
810
+
"rustls 0.23.26",
811
+
"rustls-native-certs 0.8.1",
812
+
"rustls-pki-types",
813
+
"tokio",
814
+
"tower",
815
+
"tracing",
816
+
]
817
+
818
+
[[package]]
819
+
name = "aws-smithy-json"
820
+
version = "0.61.3"
821
+
source = "registry+https://github.com/rust-lang/crates.io-index"
822
+
checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07"
823
+
dependencies = [
824
+
"aws-smithy-types",
825
+
]
826
+
827
+
[[package]]
828
+
name = "aws-smithy-observability"
829
+
version = "0.1.3"
830
+
source = "registry+https://github.com/rust-lang/crates.io-index"
831
+
checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393"
832
+
dependencies = [
833
+
"aws-smithy-runtime-api",
834
+
]
835
+
836
+
[[package]]
837
+
name = "aws-smithy-query"
838
+
version = "0.60.7"
839
+
source = "registry+https://github.com/rust-lang/crates.io-index"
840
+
checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb"
841
+
dependencies = [
842
+
"aws-smithy-types",
843
+
"urlencoding",
844
+
]
845
+
846
+
[[package]]
847
+
name = "aws-smithy-runtime"
848
+
version = "1.8.3"
849
+
source = "registry+https://github.com/rust-lang/crates.io-index"
850
+
checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e"
851
+
dependencies = [
852
+
"aws-smithy-async",
853
+
"aws-smithy-http",
854
+
"aws-smithy-http-client",
855
+
"aws-smithy-observability",
856
+
"aws-smithy-runtime-api",
857
+
"aws-smithy-types",
858
+
"bytes",
859
+
"fastrand 2.3.0",
860
+
"http 0.2.12",
861
+
"http 1.3.1",
862
+
"http-body 0.4.6",
863
+
"http-body 1.0.1",
864
+
"pin-project-lite",
865
+
"pin-utils",
866
+
"tokio",
867
+
"tracing",
868
+
]
869
+
870
+
[[package]]
871
+
name = "aws-smithy-runtime-api"
872
+
version = "1.8.0"
873
+
source = "registry+https://github.com/rust-lang/crates.io-index"
874
+
checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57"
875
+
dependencies = [
876
+
"aws-smithy-async",
877
+
"aws-smithy-types",
878
+
"bytes",
879
+
"http 0.2.12",
880
+
"http 1.3.1",
881
+
"pin-project-lite",
882
+
"tokio",
883
+
"tracing",
884
+
"zeroize",
885
+
]
886
+
887
+
[[package]]
888
+
name = "aws-smithy-types"
889
+
version = "1.3.1"
890
+
source = "registry+https://github.com/rust-lang/crates.io-index"
891
+
checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3"
892
+
dependencies = [
893
+
"base64-simd",
894
+
"bytes",
895
+
"bytes-utils",
896
+
"futures-core",
897
+
"http 0.2.12",
898
+
"http 1.3.1",
899
+
"http-body 0.4.6",
900
+
"http-body 1.0.1",
901
+
"http-body-util",
902
+
"itoa",
903
+
"num-integer",
904
+
"pin-project-lite",
905
+
"pin-utils",
906
+
"ryu",
907
+
"serde",
908
+
"time",
909
+
"tokio",
910
+
"tokio-util",
911
+
]
912
+
913
+
[[package]]
914
+
name = "aws-smithy-xml"
915
+
version = "0.60.9"
916
+
source = "registry+https://github.com/rust-lang/crates.io-index"
917
+
checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
918
+
dependencies = [
919
+
"xmlparser",
920
+
]
921
+
922
+
[[package]]
923
+
name = "aws-types"
924
+
version = "1.3.7"
925
+
source = "registry+https://github.com/rust-lang/crates.io-index"
926
+
checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a"
927
+
dependencies = [
928
+
"aws-credential-types",
929
+
"aws-smithy-async",
930
+
"aws-smithy-runtime-api",
931
+
"aws-smithy-types",
932
+
"rustc_version",
933
+
"tracing",
934
+
]
935
+
936
+
[[package]]
442
937
name = "axum"
443
938
version = "0.8.4"
444
939
source = "registry+https://github.com/rust-lang/crates.io-index"
···
450
945
"bytes",
451
946
"form_urlencoded",
452
947
"futures-util",
453
-
"http",
454
-
"http-body",
948
+
"http 1.3.1",
949
+
"http-body 1.0.1",
455
950
"http-body-util",
456
-
"hyper",
951
+
"hyper 1.6.0",
457
952
"hyper-util",
458
953
"itoa",
459
954
"matchit",
···
467
962
"serde_path_to_error",
468
963
"serde_urlencoded",
469
964
"sha1",
470
-
"sync_wrapper",
965
+
"sync_wrapper 1.0.2",
471
966
"tokio",
472
-
"tokio-tungstenite",
967
+
"tokio-tungstenite 0.26.2",
473
968
"tower",
474
969
"tower-layer",
475
970
"tower-service",
···
484
979
dependencies = [
485
980
"bytes",
486
981
"futures-core",
487
-
"http",
488
-
"http-body",
982
+
"http 1.3.1",
983
+
"http-body 1.0.1",
489
984
"http-body-util",
490
985
"mime",
491
986
"pin-project-lite",
492
987
"rustversion",
493
-
"sync_wrapper",
988
+
"sync_wrapper 1.0.2",
494
989
"tower-layer",
495
990
"tower-service",
496
991
"tracing",
···
572
1067
573
1068
[[package]]
574
1069
name = "base16ct"
1070
+
version = "0.1.1"
1071
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1072
+
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
1073
+
1074
+
[[package]]
1075
+
name = "base16ct"
575
1076
version = "0.2.0"
576
1077
source = "registry+https://github.com/rust-lang/crates.io-index"
577
1078
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
···
590
1091
591
1092
[[package]]
592
1093
name = "base64"
1094
+
version = "0.21.7"
1095
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1096
+
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
1097
+
1098
+
[[package]]
1099
+
name = "base64"
593
1100
version = "0.22.1"
594
1101
source = "registry+https://github.com/rust-lang/crates.io-index"
595
1102
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
596
1103
597
1104
[[package]]
1105
+
name = "base64-simd"
1106
+
version = "0.8.0"
1107
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1108
+
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
1109
+
dependencies = [
1110
+
"outref",
1111
+
"vsimd",
1112
+
]
1113
+
1114
+
[[package]]
1115
+
name = "base64-url"
1116
+
version = "2.0.2"
1117
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1118
+
checksum = "fb9fb9fb058cc3063b5fc88d9a21eefa2735871498a04e1650da76ed511c8569"
1119
+
dependencies = [
1120
+
"base64 0.21.7",
1121
+
]
1122
+
1123
+
[[package]]
598
1124
name = "base64ct"
599
1125
version = "1.7.3"
600
1126
source = "registry+https://github.com/rust-lang/crates.io-index"
601
1127
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
1128
+
1129
+
[[package]]
1130
+
name = "binascii"
1131
+
version = "0.1.4"
1132
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1133
+
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
602
1134
603
1135
[[package]]
604
1136
name = "bincode"
···
615
1147
source = "registry+https://github.com/rust-lang/crates.io-index"
616
1148
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
617
1149
dependencies = [
618
-
"bitflags",
1150
+
"bitflags 2.9.0",
619
1151
"cexpr",
620
1152
"clang-sys",
621
1153
"itertools",
···
633
1165
]
634
1166
635
1167
[[package]]
1168
+
name = "binstring"
1169
+
version = "0.1.6"
1170
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1171
+
checksum = "9a3a3c2603413428303761fae99d4b6d936404208221a44eba47d7c1e6dd03a3"
1172
+
1173
+
[[package]]
1174
+
name = "bit_field"
1175
+
version = "0.10.2"
1176
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1177
+
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
1178
+
1179
+
[[package]]
636
1180
name = "bitcoin-internals"
637
1181
version = "0.2.0"
638
1182
source = "registry+https://github.com/rust-lang/crates.io-index"
···
650
1194
651
1195
[[package]]
652
1196
name = "bitflags"
1197
+
version = "1.3.2"
1198
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1199
+
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
1200
+
1201
+
[[package]]
1202
+
name = "bitflags"
653
1203
version = "2.9.0"
654
1204
source = "registry+https://github.com/rust-lang/crates.io-index"
655
1205
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
656
-
dependencies = [
657
-
"serde",
658
-
]
1206
+
1207
+
[[package]]
1208
+
name = "bitstream-io"
1209
+
version = "2.6.0"
1210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1211
+
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
659
1212
660
1213
[[package]]
661
1214
name = "blake2"
···
730
1283
"anyhow",
731
1284
"argon2",
732
1285
"async-trait",
733
-
"atrium-api",
1286
+
"atrium-api 0.25.3",
734
1287
"atrium-crypto",
735
1288
"atrium-repo",
736
1289
"atrium-xrpc",
···
741
1294
"base32",
742
1295
"base64 0.22.1",
743
1296
"chrono",
1297
+
"cid 0.10.1",
744
1298
"clap",
745
1299
"clap-verbosity-flag",
746
1300
"constcat",
1301
+
"diesel",
1302
+
"diesel_migrations",
747
1303
"figment",
748
1304
"futures",
749
1305
"hex",
···
755
1311
"metrics",
756
1312
"metrics-exporter-prometheus",
757
1313
"multihash 0.19.3",
1314
+
"r2d2",
758
1315
"rand 0.8.5",
759
1316
"regex",
760
-
"reqwest",
1317
+
"reqwest 0.12.15",
761
1318
"reqwest-middleware",
1319
+
"rsky-common",
1320
+
"rsky-pds",
762
1321
"rsky-repo",
763
1322
"rsky-syntax",
764
1323
"serde",
···
767
1326
"serde_ipld_dagjson",
768
1327
"serde_json",
769
1328
"sha2",
770
-
"sqlx",
771
1329
"thiserror 2.0.12",
772
1330
"tokio",
773
1331
"tokio-util",
···
776
1334
"tracing-subscriber",
777
1335
"url",
778
1336
"urlencoding",
779
-
"uuid",
1337
+
"uuid 1.16.0",
780
1338
]
781
1339
782
1340
[[package]]
1341
+
name = "built"
1342
+
version = "0.7.7"
1343
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1344
+
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
1345
+
1346
+
[[package]]
783
1347
name = "bumpalo"
784
1348
version = "3.17.0"
785
1349
source = "registry+https://github.com/rust-lang/crates.io-index"
···
796
1360
version = "1.5.0"
797
1361
source = "registry+https://github.com/rust-lang/crates.io-index"
798
1362
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
1363
+
1364
+
[[package]]
1365
+
name = "byteorder-lite"
1366
+
version = "0.1.0"
1367
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1368
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
799
1369
800
1370
[[package]]
801
1371
name = "bytes"
···
804
1374
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
805
1375
806
1376
[[package]]
1377
+
name = "bytes-utils"
1378
+
version = "0.1.4"
1379
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1380
+
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
1381
+
dependencies = [
1382
+
"bytes",
1383
+
"either",
1384
+
]
1385
+
1386
+
[[package]]
807
1387
name = "cbor4ii"
808
1388
version = "0.2.14"
809
1389
source = "registry+https://github.com/rust-lang/crates.io-index"
···
830
1410
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
831
1411
dependencies = [
832
1412
"nom",
1413
+
]
1414
+
1415
+
[[package]]
1416
+
name = "cfb"
1417
+
version = "0.7.3"
1418
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1419
+
checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
1420
+
dependencies = [
1421
+
"byteorder",
1422
+
"fnv",
1423
+
"uuid 1.16.0",
1424
+
]
1425
+
1426
+
[[package]]
1427
+
name = "cfg-expr"
1428
+
version = "0.15.8"
1429
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1430
+
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
1431
+
dependencies = [
1432
+
"smallvec",
1433
+
"target-lexicon",
833
1434
]
834
1435
835
1436
[[package]]
···
952
1553
]
953
1554
954
1555
[[package]]
1556
+
name = "coarsetime"
1557
+
version = "0.1.36"
1558
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1559
+
checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4"
1560
+
dependencies = [
1561
+
"libc",
1562
+
"wasix",
1563
+
"wasm-bindgen",
1564
+
]
1565
+
1566
+
[[package]]
1567
+
name = "color_quant"
1568
+
version = "1.1.0"
1569
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1570
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
1571
+
1572
+
[[package]]
955
1573
name = "colorchoice"
956
1574
version = "1.0.3"
957
1575
source = "registry+https://github.com/rust-lang/crates.io-index"
···
989
1607
version = "0.6.0"
990
1608
source = "registry+https://github.com/rust-lang/crates.io-index"
991
1609
checksum = "5ffb5df6dd5dadb422897e8132f415d7a054e3cd757e5070b663f75bea1840fb"
1610
+
1611
+
[[package]]
1612
+
name = "cookie"
1613
+
version = "0.18.1"
1614
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1615
+
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
1616
+
dependencies = [
1617
+
"percent-encoding",
1618
+
"time",
1619
+
"version_check",
1620
+
]
992
1621
993
1622
[[package]]
994
1623
name = "core-foundation"
···
1050
1679
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
1051
1680
1052
1681
[[package]]
1682
+
name = "crc32c"
1683
+
version = "0.6.8"
1684
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1685
+
checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47"
1686
+
dependencies = [
1687
+
"rustc_version",
1688
+
]
1689
+
1690
+
[[package]]
1053
1691
name = "crc32fast"
1054
1692
version = "1.4.2"
1055
1693
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1059
1697
]
1060
1698
1061
1699
[[package]]
1700
+
name = "crc64fast-nvme"
1701
+
version = "1.2.0"
1702
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1703
+
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
1704
+
dependencies = [
1705
+
"crc",
1706
+
]
1707
+
1708
+
[[package]]
1062
1709
name = "crossbeam-channel"
1063
1710
version = "0.5.15"
1064
1711
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1068
1715
]
1069
1716
1070
1717
[[package]]
1071
-
name = "crossbeam-epoch"
1072
-
version = "0.9.18"
1718
+
name = "crossbeam-deque"
1719
+
version = "0.8.6"
1073
1720
source = "registry+https://github.com/rust-lang/crates.io-index"
1074
-
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
1721
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
1075
1722
dependencies = [
1723
+
"crossbeam-epoch",
1076
1724
"crossbeam-utils",
1077
1725
]
1078
1726
1079
1727
[[package]]
1080
-
name = "crossbeam-queue"
1081
-
version = "0.3.12"
1728
+
name = "crossbeam-epoch"
1729
+
version = "0.9.18"
1082
1730
source = "registry+https://github.com/rust-lang/crates.io-index"
1083
-
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
1731
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
1084
1732
dependencies = [
1085
1733
"crossbeam-utils",
1086
1734
]
···
1092
1740
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
1093
1741
1094
1742
[[package]]
1743
+
name = "crunchy"
1744
+
version = "0.2.3"
1745
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1746
+
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
1747
+
1748
+
[[package]]
1749
+
name = "crypto-bigint"
1750
+
version = "0.4.9"
1751
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1752
+
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
1753
+
dependencies = [
1754
+
"generic-array",
1755
+
"rand_core 0.6.4",
1756
+
"subtle",
1757
+
"zeroize",
1758
+
]
1759
+
1760
+
[[package]]
1095
1761
name = "crypto-bigint"
1096
1762
version = "0.5.5"
1097
1763
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1112
1778
"generic-array",
1113
1779
"typenum",
1114
1780
]
1781
+
1782
+
[[package]]
1783
+
name = "ct-codecs"
1784
+
version = "1.1.5"
1785
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1786
+
checksum = "dd0d274c65cbc1c34703d2fc2ce0fb892ff68f4516b677671a2f238a30b9b2b2"
1115
1787
1116
1788
[[package]]
1117
1789
name = "darling"
···
1190
1862
1191
1863
[[package]]
1192
1864
name = "der"
1865
+
version = "0.6.1"
1866
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1867
+
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
1868
+
dependencies = [
1869
+
"const-oid",
1870
+
"zeroize",
1871
+
]
1872
+
1873
+
[[package]]
1874
+
name = "der"
1193
1875
version = "0.7.10"
1194
1876
source = "registry+https://github.com/rust-lang/crates.io-index"
1195
1877
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
···
1241
1923
]
1242
1924
1243
1925
[[package]]
1926
+
name = "devise"
1927
+
version = "0.4.2"
1928
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1929
+
checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
1930
+
dependencies = [
1931
+
"devise_codegen",
1932
+
"devise_core",
1933
+
]
1934
+
1935
+
[[package]]
1936
+
name = "devise_codegen"
1937
+
version = "0.4.2"
1938
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1939
+
checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
1940
+
dependencies = [
1941
+
"devise_core",
1942
+
"quote",
1943
+
]
1944
+
1945
+
[[package]]
1946
+
name = "devise_core"
1947
+
version = "0.4.2"
1948
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1949
+
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
1950
+
dependencies = [
1951
+
"bitflags 2.9.0",
1952
+
"proc-macro2",
1953
+
"proc-macro2-diagnostics",
1954
+
"quote",
1955
+
"syn 2.0.101",
1956
+
]
1957
+
1958
+
[[package]]
1959
+
name = "diesel"
1960
+
version = "2.1.5"
1961
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1962
+
checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559"
1963
+
dependencies = [
1964
+
"bitflags 2.9.0",
1965
+
"byteorder",
1966
+
"chrono",
1967
+
"diesel_derives",
1968
+
"itoa",
1969
+
"libsqlite3-sys",
1970
+
"pq-sys",
1971
+
"r2d2",
1972
+
"time",
1973
+
]
1974
+
1975
+
[[package]]
1976
+
name = "diesel_derives"
1977
+
version = "2.1.4"
1978
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1979
+
checksum = "14701062d6bed917b5c7103bdffaee1e4609279e240488ad24e7bd979ca6866c"
1980
+
dependencies = [
1981
+
"diesel_table_macro_syntax",
1982
+
"proc-macro2",
1983
+
"quote",
1984
+
"syn 2.0.101",
1985
+
]
1986
+
1987
+
[[package]]
1988
+
name = "diesel_migrations"
1989
+
version = "2.1.0"
1990
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1991
+
checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
1992
+
dependencies = [
1993
+
"diesel",
1994
+
"migrations_internals",
1995
+
"migrations_macros",
1996
+
]
1997
+
1998
+
[[package]]
1999
+
name = "diesel_table_macro_syntax"
2000
+
version = "0.1.0"
2001
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2002
+
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
2003
+
dependencies = [
2004
+
"syn 2.0.101",
2005
+
]
2006
+
2007
+
[[package]]
1244
2008
name = "digest"
1245
2009
version = "0.10.7"
1246
2010
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1283
2047
1284
2048
[[package]]
1285
2049
name = "ecdsa"
2050
+
version = "0.14.8"
2051
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2052
+
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
2053
+
dependencies = [
2054
+
"der 0.6.1",
2055
+
"elliptic-curve 0.12.3",
2056
+
"rfc6979 0.3.1",
2057
+
"signature 1.6.4",
2058
+
]
2059
+
2060
+
[[package]]
2061
+
name = "ecdsa"
1286
2062
version = "0.16.9"
1287
2063
source = "registry+https://github.com/rust-lang/crates.io-index"
1288
2064
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
1289
2065
dependencies = [
1290
-
"der",
2066
+
"der 0.7.10",
1291
2067
"digest",
1292
-
"elliptic-curve",
1293
-
"rfc6979",
1294
-
"signature",
1295
-
"spki",
2068
+
"elliptic-curve 0.13.8",
2069
+
"rfc6979 0.4.0",
2070
+
"signature 2.2.0",
2071
+
"spki 0.7.3",
2072
+
]
2073
+
2074
+
[[package]]
2075
+
name = "ed25519-compact"
2076
+
version = "2.1.1"
2077
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2078
+
checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190"
2079
+
dependencies = [
2080
+
"ct-codecs",
2081
+
"getrandom 0.2.16",
1296
2082
]
1297
2083
1298
2084
[[package]]
···
1300
2086
version = "1.15.0"
1301
2087
source = "registry+https://github.com/rust-lang/crates.io-index"
1302
2088
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
2089
+
2090
+
[[package]]
2091
+
name = "elliptic-curve"
2092
+
version = "0.12.3"
2093
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2094
+
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
1303
2095
dependencies = [
1304
-
"serde",
2096
+
"base16ct 0.1.1",
2097
+
"crypto-bigint 0.4.9",
2098
+
"der 0.6.1",
2099
+
"digest",
2100
+
"ff 0.12.1",
2101
+
"generic-array",
2102
+
"group 0.12.1",
2103
+
"pkcs8 0.9.0",
2104
+
"rand_core 0.6.4",
2105
+
"sec1 0.3.0",
2106
+
"subtle",
2107
+
"zeroize",
1305
2108
]
1306
2109
1307
2110
[[package]]
···
1310
2113
source = "registry+https://github.com/rust-lang/crates.io-index"
1311
2114
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
1312
2115
dependencies = [
1313
-
"base16ct",
1314
-
"crypto-bigint",
2116
+
"base16ct 0.2.0",
2117
+
"crypto-bigint 0.5.5",
1315
2118
"digest",
1316
-
"ff",
2119
+
"ff 0.13.1",
1317
2120
"generic-array",
1318
-
"group",
2121
+
"group 0.13.0",
2122
+
"hkdf",
1319
2123
"pem-rfc7468",
1320
-
"pkcs8",
2124
+
"pkcs8 0.10.2",
1321
2125
"rand_core 0.6.4",
1322
-
"sec1",
2126
+
"sec1 0.7.3",
1323
2127
"subtle",
1324
2128
"zeroize",
1325
2129
]
1326
2130
1327
2131
[[package]]
2132
+
name = "email_address"
2133
+
version = "0.2.9"
2134
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2135
+
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
2136
+
dependencies = [
2137
+
"serde",
2138
+
]
2139
+
2140
+
[[package]]
1328
2141
name = "encoding_rs"
1329
2142
version = "0.8.35"
1330
2143
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1362
2175
]
1363
2176
1364
2177
[[package]]
1365
-
name = "etcetera"
1366
-
version = "0.8.0"
2178
+
name = "event-emitter-rs"
2179
+
version = "0.1.4"
1367
2180
source = "registry+https://github.com/rust-lang/crates.io-index"
1368
-
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
2181
+
checksum = "7dccdd0a59457ba353cc44c26d431ca5089f2cf93035c780a1b3f2814a017ebd"
1369
2182
dependencies = [
1370
-
"cfg-if",
1371
-
"home",
1372
-
"windows-sys 0.48.0",
2183
+
"bincode",
2184
+
"lazy_static",
2185
+
"serde",
2186
+
"uuid 0.8.2",
1373
2187
]
1374
2188
1375
2189
[[package]]
···
1400
2214
]
1401
2215
1402
2216
[[package]]
2217
+
name = "exr"
2218
+
version = "1.73.0"
2219
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2220
+
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
2221
+
dependencies = [
2222
+
"bit_field",
2223
+
"half 2.6.0",
2224
+
"lebe",
2225
+
"miniz_oxide",
2226
+
"rayon-core",
2227
+
"smallvec",
2228
+
"zune-inflate",
2229
+
]
2230
+
2231
+
[[package]]
2232
+
name = "fast_chemail"
2233
+
version = "0.9.6"
2234
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2235
+
checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
2236
+
dependencies = [
2237
+
"ascii_utils",
2238
+
]
2239
+
2240
+
[[package]]
1403
2241
name = "fastrand"
1404
2242
version = "1.9.0"
1405
2243
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1415
2253
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1416
2254
1417
2255
[[package]]
2256
+
name = "fdeflate"
2257
+
version = "0.3.7"
2258
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2259
+
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
2260
+
dependencies = [
2261
+
"simd-adler32",
2262
+
]
2263
+
2264
+
[[package]]
2265
+
name = "ff"
2266
+
version = "0.12.1"
2267
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2268
+
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
2269
+
dependencies = [
2270
+
"rand_core 0.6.4",
2271
+
"subtle",
2272
+
]
2273
+
2274
+
[[package]]
1418
2275
name = "ff"
1419
2276
version = "0.13.1"
1420
2277
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1430
2287
source = "registry+https://github.com/rust-lang/crates.io-index"
1431
2288
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
1432
2289
dependencies = [
1433
-
"atomic",
2290
+
"atomic 0.6.0",
1434
2291
"pear",
1435
2292
"serde",
1436
2293
"toml 0.8.22",
···
1446
2303
dependencies = [
1447
2304
"crc32fast",
1448
2305
"miniz_oxide",
1449
-
]
1450
-
1451
-
[[package]]
1452
-
name = "flume"
1453
-
version = "0.11.1"
1454
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1455
-
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
1456
-
dependencies = [
1457
-
"futures-core",
1458
-
"futures-sink",
1459
-
"spin",
1460
2306
]
1461
2307
1462
2308
[[package]]
···
1544
2390
]
1545
2391
1546
2392
[[package]]
1547
-
name = "futures-intrusive"
1548
-
version = "0.5.0"
1549
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1550
-
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
1551
-
dependencies = [
1552
-
"futures-core",
1553
-
"lock_api",
1554
-
"parking_lot",
1555
-
]
1556
-
1557
-
[[package]]
1558
2393
name = "futures-io"
1559
2394
version = "0.3.31"
1560
2395
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1631
2466
1632
2467
[[package]]
1633
2468
name = "generator"
2469
+
version = "0.7.5"
2470
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2471
+
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
2472
+
dependencies = [
2473
+
"cc",
2474
+
"libc",
2475
+
"log",
2476
+
"rustversion",
2477
+
"windows 0.48.0",
2478
+
]
2479
+
2480
+
[[package]]
2481
+
name = "generator"
1634
2482
version = "0.8.4"
1635
2483
source = "registry+https://github.com/rust-lang/crates.io-index"
1636
2484
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
···
1639
2487
"libc",
1640
2488
"log",
1641
2489
"rustversion",
1642
-
"windows",
2490
+
"windows 0.58.0",
1643
2491
]
1644
2492
1645
2493
[[package]]
···
1690
2538
]
1691
2539
1692
2540
[[package]]
2541
+
name = "gif"
2542
+
version = "0.13.1"
2543
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2544
+
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
2545
+
dependencies = [
2546
+
"color_quant",
2547
+
"weezl",
2548
+
]
2549
+
2550
+
[[package]]
1693
2551
name = "gimli"
1694
2552
version = "0.31.1"
1695
2553
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1703
2561
1704
2562
[[package]]
1705
2563
name = "group"
2564
+
version = "0.12.1"
2565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2566
+
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
2567
+
dependencies = [
2568
+
"ff 0.12.1",
2569
+
"rand_core 0.6.4",
2570
+
"subtle",
2571
+
]
2572
+
2573
+
[[package]]
2574
+
name = "group"
1706
2575
version = "0.13.0"
1707
2576
source = "registry+https://github.com/rust-lang/crates.io-index"
1708
2577
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
1709
2578
dependencies = [
1710
-
"ff",
2579
+
"ff 0.13.1",
1711
2580
"rand_core 0.6.4",
1712
2581
"subtle",
1713
2582
]
1714
2583
1715
2584
[[package]]
1716
2585
name = "h2"
2586
+
version = "0.3.26"
2587
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2588
+
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
2589
+
dependencies = [
2590
+
"bytes",
2591
+
"fnv",
2592
+
"futures-core",
2593
+
"futures-sink",
2594
+
"futures-util",
2595
+
"http 0.2.12",
2596
+
"indexmap 2.9.0",
2597
+
"slab",
2598
+
"tokio",
2599
+
"tokio-util",
2600
+
"tracing",
2601
+
]
2602
+
2603
+
[[package]]
2604
+
name = "h2"
1717
2605
version = "0.4.9"
1718
2606
source = "registry+https://github.com/rust-lang/crates.io-index"
1719
2607
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
···
1723
2611
"fnv",
1724
2612
"futures-core",
1725
2613
"futures-sink",
1726
-
"http",
2614
+
"http 1.3.1",
1727
2615
"indexmap 2.9.0",
1728
2616
"slab",
1729
2617
"tokio",
···
1736
2624
version = "1.8.3"
1737
2625
source = "registry+https://github.com/rust-lang/crates.io-index"
1738
2626
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
2627
+
2628
+
[[package]]
2629
+
name = "half"
2630
+
version = "2.6.0"
2631
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2632
+
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
2633
+
dependencies = [
2634
+
"cfg-if",
2635
+
"crunchy",
2636
+
]
1739
2637
1740
2638
[[package]]
1741
2639
name = "hashbrown"
···
1761
2659
]
1762
2660
1763
2661
[[package]]
1764
-
name = "hashlink"
1765
-
version = "0.10.0"
1766
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1767
-
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
1768
-
dependencies = [
1769
-
"hashbrown 0.15.3",
1770
-
]
1771
-
1772
-
[[package]]
1773
2662
name = "heck"
1774
2663
version = "0.5.0"
1775
2664
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1777
2666
1778
2667
[[package]]
1779
2668
name = "hermit-abi"
2669
+
version = "0.3.9"
2670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2671
+
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
2672
+
2673
+
[[package]]
2674
+
name = "hermit-abi"
1780
2675
version = "0.4.0"
1781
2676
source = "registry+https://github.com/rust-lang/crates.io-index"
1782
2677
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
2678
+
2679
+
[[package]]
2680
+
name = "hermit-abi"
2681
+
version = "0.5.1"
2682
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2683
+
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
1783
2684
1784
2685
[[package]]
1785
2686
name = "hex"
···
1857
2758
]
1858
2759
1859
2760
[[package]]
2761
+
name = "hmac-sha1-compact"
2762
+
version = "1.1.5"
2763
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2764
+
checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3"
2765
+
2766
+
[[package]]
2767
+
name = "hmac-sha256"
2768
+
version = "1.1.8"
2769
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2770
+
checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb"
2771
+
dependencies = [
2772
+
"digest",
2773
+
]
2774
+
2775
+
[[package]]
2776
+
name = "hmac-sha512"
2777
+
version = "1.1.6"
2778
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2779
+
checksum = "b0b3a0f572aa8389d325f5852b9e0a333a15b0f86ecccbb3fdb6e97cd86dc67c"
2780
+
dependencies = [
2781
+
"digest",
2782
+
]
2783
+
2784
+
[[package]]
1860
2785
name = "home"
1861
2786
version = "0.5.11"
1862
2787
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1867
2792
1868
2793
[[package]]
1869
2794
name = "http"
2795
+
version = "0.2.12"
2796
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2797
+
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
2798
+
dependencies = [
2799
+
"bytes",
2800
+
"fnv",
2801
+
"itoa",
2802
+
]
2803
+
2804
+
[[package]]
2805
+
name = "http"
1870
2806
version = "1.3.1"
1871
2807
source = "registry+https://github.com/rust-lang/crates.io-index"
1872
2808
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
···
1878
2814
1879
2815
[[package]]
1880
2816
name = "http-body"
2817
+
version = "0.4.6"
2818
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2819
+
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
2820
+
dependencies = [
2821
+
"bytes",
2822
+
"http 0.2.12",
2823
+
"pin-project-lite",
2824
+
]
2825
+
2826
+
[[package]]
2827
+
name = "http-body"
1881
2828
version = "1.0.1"
1882
2829
source = "registry+https://github.com/rust-lang/crates.io-index"
1883
2830
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
1884
2831
dependencies = [
1885
2832
"bytes",
1886
-
"http",
2833
+
"http 1.3.1",
1887
2834
]
1888
2835
1889
2836
[[package]]
···
1894
2841
dependencies = [
1895
2842
"bytes",
1896
2843
"futures-core",
1897
-
"http",
1898
-
"http-body",
2844
+
"http 1.3.1",
2845
+
"http-body 1.0.1",
1899
2846
"pin-project-lite",
1900
2847
]
1901
2848
···
1907
2854
dependencies = [
1908
2855
"async-trait",
1909
2856
"bincode",
1910
-
"http",
2857
+
"http 1.3.1",
1911
2858
"http-cache-semantics",
1912
2859
"httpdate",
1913
2860
"moka",
···
1923
2870
dependencies = [
1924
2871
"anyhow",
1925
2872
"async-trait",
1926
-
"http",
2873
+
"http 1.3.1",
1927
2874
"http-cache",
1928
2875
"http-cache-semantics",
1929
-
"reqwest",
2876
+
"reqwest 0.12.15",
1930
2877
"reqwest-middleware",
1931
2878
"serde",
1932
2879
"url",
···
1938
2885
source = "registry+https://github.com/rust-lang/crates.io-index"
1939
2886
checksum = "92baf25cf0b8c9246baecf3a444546360a97b569168fdf92563ee6a47829920c"
1940
2887
dependencies = [
1941
-
"http",
2888
+
"http 1.3.1",
1942
2889
"http-serde",
1943
2890
"serde",
1944
2891
"time",
···
1956
2903
source = "registry+https://github.com/rust-lang/crates.io-index"
1957
2904
checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd"
1958
2905
dependencies = [
1959
-
"http",
2906
+
"http 1.3.1",
1960
2907
"serde",
1961
2908
]
1962
2909
···
1970
2917
"async-channel 1.9.0",
1971
2918
"base64 0.13.1",
1972
2919
"futures-lite 1.13.0",
1973
-
"infer",
2920
+
"infer 0.2.3",
1974
2921
"pin-project-lite",
1975
2922
"rand 0.7.3",
1976
2923
"serde",
···
1994
2941
1995
2942
[[package]]
1996
2943
name = "hyper"
2944
+
version = "0.14.32"
2945
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2946
+
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
2947
+
dependencies = [
2948
+
"bytes",
2949
+
"futures-channel",
2950
+
"futures-core",
2951
+
"futures-util",
2952
+
"h2 0.3.26",
2953
+
"http 0.2.12",
2954
+
"http-body 0.4.6",
2955
+
"httparse",
2956
+
"httpdate",
2957
+
"itoa",
2958
+
"pin-project-lite",
2959
+
"socket2",
2960
+
"tokio",
2961
+
"tower-service",
2962
+
"tracing",
2963
+
"want",
2964
+
]
2965
+
2966
+
[[package]]
2967
+
name = "hyper"
1997
2968
version = "1.6.0"
1998
2969
source = "registry+https://github.com/rust-lang/crates.io-index"
1999
2970
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
···
2001
2972
"bytes",
2002
2973
"futures-channel",
2003
2974
"futures-util",
2004
-
"h2",
2005
-
"http",
2006
-
"http-body",
2975
+
"h2 0.4.9",
2976
+
"http 1.3.1",
2977
+
"http-body 1.0.1",
2007
2978
"httparse",
2008
2979
"httpdate",
2009
2980
"itoa",
···
2015
2986
2016
2987
[[package]]
2017
2988
name = "hyper-rustls"
2989
+
version = "0.24.2"
2990
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2991
+
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
2992
+
dependencies = [
2993
+
"futures-util",
2994
+
"http 0.2.12",
2995
+
"hyper 0.14.32",
2996
+
"log",
2997
+
"rustls 0.21.12",
2998
+
"rustls-native-certs 0.6.3",
2999
+
"tokio",
3000
+
"tokio-rustls 0.24.1",
3001
+
]
3002
+
3003
+
[[package]]
3004
+
name = "hyper-rustls"
2018
3005
version = "0.27.5"
2019
3006
source = "registry+https://github.com/rust-lang/crates.io-index"
2020
3007
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
2021
3008
dependencies = [
2022
3009
"futures-util",
2023
-
"http",
2024
-
"hyper",
3010
+
"http 1.3.1",
3011
+
"hyper 1.6.0",
2025
3012
"hyper-util",
2026
-
"rustls",
2027
-
"rustls-native-certs",
3013
+
"rustls 0.23.26",
3014
+
"rustls-native-certs 0.8.1",
2028
3015
"rustls-pki-types",
2029
3016
"tokio",
2030
-
"tokio-rustls",
3017
+
"tokio-rustls 0.26.2",
2031
3018
"tower-service",
2032
3019
"webpki-roots 0.26.11",
2033
3020
]
···
2040
3027
dependencies = [
2041
3028
"bytes",
2042
3029
"http-body-util",
2043
-
"hyper",
3030
+
"hyper 1.6.0",
2044
3031
"hyper-util",
2045
3032
"native-tls",
2046
3033
"tokio",
···
2057
3044
"bytes",
2058
3045
"futures-channel",
2059
3046
"futures-util",
2060
-
"http",
2061
-
"http-body",
2062
-
"hyper",
3047
+
"http 1.3.1",
3048
+
"http-body 1.0.1",
3049
+
"hyper 1.6.0",
2063
3050
"libc",
2064
3051
"pin-project-lite",
2065
3052
"socket2",
···
2238
3225
]
2239
3226
2240
3227
[[package]]
3228
+
name = "image"
3229
+
version = "0.25.6"
3230
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3231
+
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
3232
+
dependencies = [
3233
+
"bytemuck",
3234
+
"byteorder-lite",
3235
+
"color_quant",
3236
+
"exr",
3237
+
"gif",
3238
+
"image-webp",
3239
+
"num-traits",
3240
+
"png",
3241
+
"qoi",
3242
+
"ravif",
3243
+
"rayon",
3244
+
"rgb",
3245
+
"tiff",
3246
+
"zune-core",
3247
+
"zune-jpeg",
3248
+
]
3249
+
3250
+
[[package]]
3251
+
name = "image-webp"
3252
+
version = "0.2.1"
3253
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3254
+
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
3255
+
dependencies = [
3256
+
"byteorder-lite",
3257
+
"quick-error",
3258
+
]
3259
+
3260
+
[[package]]
3261
+
name = "imgref"
3262
+
version = "1.11.0"
3263
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3264
+
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
3265
+
3266
+
[[package]]
2241
3267
name = "indexmap"
2242
3268
version = "1.9.3"
2243
3269
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2256
3282
dependencies = [
2257
3283
"equivalent",
2258
3284
"hashbrown 0.15.3",
3285
+
"serde",
2259
3286
]
2260
3287
2261
3288
[[package]]
···
2265
3292
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
2266
3293
2267
3294
[[package]]
3295
+
name = "infer"
3296
+
version = "0.15.0"
3297
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3298
+
checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
3299
+
dependencies = [
3300
+
"cfb",
3301
+
]
3302
+
3303
+
[[package]]
2268
3304
name = "inlinable_string"
2269
3305
version = "0.1.15"
2270
3306
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2290
3326
]
2291
3327
2292
3328
[[package]]
3329
+
name = "interpolate_name"
3330
+
version = "0.2.4"
3331
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3332
+
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
3333
+
dependencies = [
3334
+
"proc-macro2",
3335
+
"quote",
3336
+
"syn 2.0.101",
3337
+
]
3338
+
3339
+
[[package]]
2293
3340
name = "ipconfig"
2294
3341
version = "0.3.2"
2295
3342
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2319
3366
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
2320
3367
2321
3368
[[package]]
3369
+
name = "is-terminal"
3370
+
version = "0.4.16"
3371
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3372
+
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
3373
+
dependencies = [
3374
+
"hermit-abi 0.5.1",
3375
+
"libc",
3376
+
"windows-sys 0.59.0",
3377
+
]
3378
+
3379
+
[[package]]
2322
3380
name = "is_terminal_polyfill"
2323
3381
version = "1.70.1"
2324
3382
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2350
3408
]
2351
3409
2352
3410
[[package]]
3411
+
name = "jpeg-decoder"
3412
+
version = "0.3.1"
3413
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3414
+
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
3415
+
3416
+
[[package]]
2353
3417
name = "js-sys"
2354
3418
version = "0.3.77"
2355
3419
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2360
3424
]
2361
3425
2362
3426
[[package]]
3427
+
name = "jwt-simple"
3428
+
version = "0.12.12"
3429
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3430
+
checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a"
3431
+
dependencies = [
3432
+
"anyhow",
3433
+
"binstring",
3434
+
"blake2b_simd",
3435
+
"coarsetime",
3436
+
"ct-codecs",
3437
+
"ed25519-compact",
3438
+
"hmac-sha1-compact",
3439
+
"hmac-sha256",
3440
+
"hmac-sha512",
3441
+
"k256",
3442
+
"p256 0.13.2",
3443
+
"p384",
3444
+
"rand 0.8.5",
3445
+
"serde",
3446
+
"serde_json",
3447
+
"superboring",
3448
+
"thiserror 2.0.12",
3449
+
"zeroize",
3450
+
]
3451
+
3452
+
[[package]]
2363
3453
name = "k256"
2364
3454
version = "0.13.4"
2365
3455
source = "registry+https://github.com/rust-lang/crates.io-index"
2366
3456
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
2367
3457
dependencies = [
2368
3458
"cfg-if",
2369
-
"ecdsa",
2370
-
"elliptic-curve",
3459
+
"ecdsa 0.16.9",
3460
+
"elliptic-curve 0.13.8",
2371
3461
"once_cell",
2372
3462
"sha2",
2373
-
"signature",
3463
+
"signature 2.2.0",
2374
3464
]
2375
3465
2376
3466
[[package]]
···
2407
3497
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
2408
3498
2409
3499
[[package]]
3500
+
name = "lebe"
3501
+
version = "0.5.2"
3502
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3503
+
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
3504
+
3505
+
[[package]]
2410
3506
name = "libc"
2411
3507
version = "0.2.172"
2412
3508
source = "registry+https://github.com/rust-lang/crates.io-index"
2413
3509
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
3510
+
3511
+
[[package]]
3512
+
name = "libfuzzer-sys"
3513
+
version = "0.4.9"
3514
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3515
+
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
3516
+
dependencies = [
3517
+
"arbitrary",
3518
+
"cc",
3519
+
]
2414
3520
2415
3521
[[package]]
2416
3522
name = "libipld"
···
2519
3625
2520
3626
[[package]]
2521
3627
name = "libsqlite3-sys"
2522
-
version = "0.30.1"
3628
+
version = "0.28.0"
2523
3629
source = "registry+https://github.com/rust-lang/crates.io-index"
2524
-
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
3630
+
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
2525
3631
dependencies = [
2526
-
"cc",
2527
3632
"pkg-config",
2528
3633
"vcpkg",
2529
3634
]
···
2570
3675
2571
3676
[[package]]
2572
3677
name = "loom"
3678
+
version = "0.5.6"
3679
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3680
+
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
3681
+
dependencies = [
3682
+
"cfg-if",
3683
+
"generator 0.7.5",
3684
+
"scoped-tls",
3685
+
"serde",
3686
+
"serde_json",
3687
+
"tracing",
3688
+
"tracing-subscriber",
3689
+
]
3690
+
3691
+
[[package]]
3692
+
name = "loom"
2573
3693
version = "0.7.2"
2574
3694
source = "registry+https://github.com/rust-lang/crates.io-index"
2575
3695
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
2576
3696
dependencies = [
2577
3697
"cfg-if",
2578
-
"generator",
3698
+
"generator 0.8.4",
2579
3699
"scoped-tls",
2580
3700
"tracing",
2581
3701
"tracing-subscriber",
2582
3702
]
2583
3703
2584
3704
[[package]]
3705
+
name = "loop9"
3706
+
version = "0.1.5"
3707
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3708
+
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
3709
+
dependencies = [
3710
+
"imgref",
3711
+
]
3712
+
3713
+
[[package]]
2585
3714
name = "lru"
2586
3715
version = "0.12.5"
2587
3716
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2600
3729
]
2601
3730
2602
3731
[[package]]
3732
+
name = "mailchecker"
3733
+
version = "6.0.17"
3734
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3735
+
checksum = "db3c69370540384985601e4adbbbc3046a658853e4909a4bd744bb390f6f9759"
3736
+
dependencies = [
3737
+
"fast_chemail",
3738
+
"once_cell",
3739
+
]
3740
+
3741
+
[[package]]
3742
+
name = "mailgun-rs"
3743
+
version = "0.1.12"
3744
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3745
+
checksum = "e7ce77c6c4195bac30129f854bdff000dee52dddaa7913b6c73df7e7f062398d"
3746
+
dependencies = [
3747
+
"reqwest 0.11.27",
3748
+
"serde",
3749
+
"serde_json",
3750
+
"typed-builder",
3751
+
]
3752
+
3753
+
[[package]]
2603
3754
name = "matchers"
2604
3755
version = "0.1.0"
2605
3756
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2615
3766
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
2616
3767
2617
3768
[[package]]
3769
+
name = "maybe-rayon"
3770
+
version = "0.1.1"
3771
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3772
+
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
3773
+
dependencies = [
3774
+
"cfg-if",
3775
+
"rayon",
3776
+
]
3777
+
3778
+
[[package]]
2618
3779
name = "md-5"
2619
3780
version = "0.10.6"
2620
3781
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2657
3818
dependencies = [
2658
3819
"base64 0.22.1",
2659
3820
"http-body-util",
2660
-
"hyper",
2661
-
"hyper-rustls",
3821
+
"hyper 1.6.0",
3822
+
"hyper-rustls 0.27.5",
2662
3823
"hyper-util",
2663
3824
"indexmap 2.9.0",
2664
3825
"ipnet",
···
2710
3871
]
2711
3872
2712
3873
[[package]]
3874
+
name = "migrations_internals"
3875
+
version = "2.1.0"
3876
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3877
+
checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
3878
+
dependencies = [
3879
+
"serde",
3880
+
"toml 0.7.8",
3881
+
]
3882
+
3883
+
[[package]]
3884
+
name = "migrations_macros"
3885
+
version = "2.1.0"
3886
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3887
+
checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
3888
+
dependencies = [
3889
+
"migrations_internals",
3890
+
"proc-macro2",
3891
+
"quote",
3892
+
]
3893
+
3894
+
[[package]]
2713
3895
name = "mime"
2714
3896
version = "0.3.17"
2715
3897
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2738
3920
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
2739
3921
dependencies = [
2740
3922
"adler2",
3923
+
"simd-adler32",
2741
3924
]
2742
3925
2743
3926
[[package]]
···
2763
3946
"crossbeam-utils",
2764
3947
"event-listener 5.4.0",
2765
3948
"futures-util",
2766
-
"loom",
3949
+
"loom 0.7.2",
2767
3950
"parking_lot",
2768
3951
"portable-atomic",
2769
3952
"rustc_version",
2770
3953
"smallvec",
2771
3954
"tagptr",
2772
3955
"thiserror 1.0.69",
2773
-
"uuid",
3956
+
"uuid 1.16.0",
3957
+
]
3958
+
3959
+
[[package]]
3960
+
name = "multer"
3961
+
version = "3.1.0"
3962
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3963
+
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
3964
+
dependencies = [
3965
+
"bytes",
3966
+
"encoding_rs",
3967
+
"futures-util",
3968
+
"http 1.3.1",
3969
+
"httparse",
3970
+
"memchr",
3971
+
"mime",
3972
+
"spin",
3973
+
"tokio",
3974
+
"tokio-util",
3975
+
"version_check",
2774
3976
]
2775
3977
2776
3978
[[package]]
···
2846
4048
]
2847
4049
2848
4050
[[package]]
4051
+
name = "new_debug_unreachable"
4052
+
version = "1.0.6"
4053
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4054
+
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
4055
+
4056
+
[[package]]
2849
4057
name = "nom"
2850
4058
version = "7.1.3"
2851
4059
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2856
4064
]
2857
4065
2858
4066
[[package]]
4067
+
name = "noop_proc_macro"
4068
+
version = "0.3.0"
4069
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4070
+
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
4071
+
4072
+
[[package]]
2859
4073
name = "nu-ansi-term"
2860
4074
version = "0.46.0"
2861
4075
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2866
4080
]
2867
4081
2868
4082
[[package]]
4083
+
name = "num-bigint"
4084
+
version = "0.4.6"
4085
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4086
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
4087
+
dependencies = [
4088
+
"num-integer",
4089
+
"num-traits",
4090
+
]
4091
+
4092
+
[[package]]
2869
4093
name = "num-bigint-dig"
2870
4094
version = "0.8.4"
2871
4095
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2889
4113
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
2890
4114
2891
4115
[[package]]
4116
+
name = "num-derive"
4117
+
version = "0.4.2"
4118
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4119
+
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
4120
+
dependencies = [
4121
+
"proc-macro2",
4122
+
"quote",
4123
+
"syn 2.0.101",
4124
+
]
4125
+
4126
+
[[package]]
2892
4127
name = "num-integer"
2893
4128
version = "0.1.46"
2894
4129
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2909
4144
]
2910
4145
2911
4146
[[package]]
4147
+
name = "num-rational"
4148
+
version = "0.4.2"
4149
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4150
+
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
4151
+
dependencies = [
4152
+
"num-bigint",
4153
+
"num-integer",
4154
+
"num-traits",
4155
+
]
4156
+
4157
+
[[package]]
2912
4158
name = "num-traits"
2913
4159
version = "0.2.19"
2914
4160
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2919
4165
]
2920
4166
2921
4167
[[package]]
4168
+
name = "num_cpus"
4169
+
version = "1.16.0"
4170
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4171
+
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
4172
+
dependencies = [
4173
+
"hermit-abi 0.3.9",
4174
+
"libc",
4175
+
]
4176
+
4177
+
[[package]]
2922
4178
name = "num_threads"
2923
4179
version = "0.1.7"
2924
4180
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2936
4192
"base64 0.22.1",
2937
4193
"chrono",
2938
4194
"getrandom 0.2.16",
2939
-
"http",
4195
+
"http 1.3.1",
2940
4196
"rand 0.8.5",
2941
4197
"serde",
2942
4198
"serde_json",
···
2967
4223
source = "registry+https://github.com/rust-lang/crates.io-index"
2968
4224
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
2969
4225
dependencies = [
2970
-
"bitflags",
4226
+
"bitflags 2.9.0",
2971
4227
"cfg-if",
2972
4228
"foreign-types",
2973
4229
"libc",
···
3006
4262
]
3007
4263
3008
4264
[[package]]
4265
+
name = "outref"
4266
+
version = "0.5.2"
4267
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4268
+
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
4269
+
4270
+
[[package]]
3009
4271
name = "overload"
3010
4272
version = "0.1.1"
3011
4273
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3013
4275
3014
4276
[[package]]
3015
4277
name = "p256"
4278
+
version = "0.11.1"
4279
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4280
+
checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
4281
+
dependencies = [
4282
+
"ecdsa 0.14.8",
4283
+
"elliptic-curve 0.12.3",
4284
+
"sha2",
4285
+
]
4286
+
4287
+
[[package]]
4288
+
name = "p256"
3016
4289
version = "0.13.2"
3017
4290
source = "registry+https://github.com/rust-lang/crates.io-index"
3018
4291
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
3019
4292
dependencies = [
3020
-
"ecdsa",
3021
-
"elliptic-curve",
4293
+
"ecdsa 0.16.9",
4294
+
"elliptic-curve 0.13.8",
4295
+
"primeorder",
4296
+
"sha2",
4297
+
]
4298
+
4299
+
[[package]]
4300
+
name = "p384"
4301
+
version = "0.13.1"
4302
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4303
+
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
4304
+
dependencies = [
4305
+
"ecdsa 0.16.9",
4306
+
"elliptic-curve 0.13.8",
3022
4307
"primeorder",
3023
4308
"sha2",
3024
4309
]
···
3156
4441
source = "registry+https://github.com/rust-lang/crates.io-index"
3157
4442
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
3158
4443
dependencies = [
3159
-
"der",
3160
-
"pkcs8",
3161
-
"spki",
4444
+
"der 0.7.10",
4445
+
"pkcs8 0.10.2",
4446
+
"spki 0.7.3",
4447
+
]
4448
+
4449
+
[[package]]
4450
+
name = "pkcs8"
4451
+
version = "0.9.0"
4452
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4453
+
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
4454
+
dependencies = [
4455
+
"der 0.6.1",
4456
+
"spki 0.6.0",
3162
4457
]
3163
4458
3164
4459
[[package]]
···
3167
4462
source = "registry+https://github.com/rust-lang/crates.io-index"
3168
4463
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
3169
4464
dependencies = [
3170
-
"der",
3171
-
"spki",
4465
+
"der 0.7.10",
4466
+
"spki 0.7.3",
3172
4467
]
3173
4468
3174
4469
[[package]]
···
3176
4471
version = "0.3.32"
3177
4472
source = "registry+https://github.com/rust-lang/crates.io-index"
3178
4473
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
4474
+
4475
+
[[package]]
4476
+
name = "png"
4477
+
version = "0.17.16"
4478
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4479
+
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
4480
+
dependencies = [
4481
+
"bitflags 1.3.2",
4482
+
"crc32fast",
4483
+
"fdeflate",
4484
+
"flate2",
4485
+
"miniz_oxide",
4486
+
]
3179
4487
3180
4488
[[package]]
3181
4489
name = "polling"
···
3185
4493
dependencies = [
3186
4494
"cfg-if",
3187
4495
"concurrent-queue",
3188
-
"hermit-abi",
4496
+
"hermit-abi 0.4.0",
3189
4497
"pin-project-lite",
3190
4498
"rustix 0.38.44",
3191
4499
"tracing",
···
3211
4519
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
3212
4520
dependencies = [
3213
4521
"zerocopy 0.8.25",
4522
+
]
4523
+
4524
+
[[package]]
4525
+
name = "pq-sys"
4526
+
version = "0.4.8"
4527
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4528
+
checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd"
4529
+
dependencies = [
4530
+
"vcpkg",
3214
4531
]
3215
4532
3216
4533
[[package]]
···
3229
4546
source = "registry+https://github.com/rust-lang/crates.io-index"
3230
4547
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
3231
4548
dependencies = [
3232
-
"elliptic-curve",
4549
+
"elliptic-curve 0.13.8",
3233
4550
]
3234
4551
3235
4552
[[package]]
···
3289
4606
]
3290
4607
3291
4608
[[package]]
4609
+
name = "profiling"
4610
+
version = "1.0.16"
4611
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4612
+
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
4613
+
dependencies = [
4614
+
"profiling-procmacros",
4615
+
]
4616
+
4617
+
[[package]]
4618
+
name = "profiling-procmacros"
4619
+
version = "1.0.16"
4620
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4621
+
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
4622
+
dependencies = [
4623
+
"quote",
4624
+
"syn 2.0.101",
4625
+
]
4626
+
4627
+
[[package]]
4628
+
name = "qoi"
4629
+
version = "0.4.1"
4630
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4631
+
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
4632
+
dependencies = [
4633
+
"bytemuck",
4634
+
]
4635
+
4636
+
[[package]]
3292
4637
name = "quanta"
3293
4638
version = "0.12.5"
3294
4639
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3302
4647
"web-sys",
3303
4648
"winapi",
3304
4649
]
4650
+
4651
+
[[package]]
4652
+
name = "quick-error"
4653
+
version = "2.0.1"
4654
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4655
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
3305
4656
3306
4657
[[package]]
3307
4658
name = "quick-protobuf"
···
3326
4677
version = "5.2.0"
3327
4678
source = "registry+https://github.com/rust-lang/crates.io-index"
3328
4679
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
4680
+
4681
+
[[package]]
4682
+
name = "r2d2"
4683
+
version = "0.8.10"
4684
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4685
+
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
4686
+
dependencies = [
4687
+
"log",
4688
+
"parking_lot",
4689
+
"scheduled-thread-pool",
4690
+
]
3329
4691
3330
4692
[[package]]
3331
4693
name = "rand"
···
3437
4799
]
3438
4800
3439
4801
[[package]]
4802
+
name = "rav1e"
4803
+
version = "0.7.1"
4804
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4805
+
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
4806
+
dependencies = [
4807
+
"arbitrary",
4808
+
"arg_enum_proc_macro",
4809
+
"arrayvec",
4810
+
"av1-grain",
4811
+
"bitstream-io",
4812
+
"built",
4813
+
"cfg-if",
4814
+
"interpolate_name",
4815
+
"itertools",
4816
+
"libc",
4817
+
"libfuzzer-sys",
4818
+
"log",
4819
+
"maybe-rayon",
4820
+
"new_debug_unreachable",
4821
+
"noop_proc_macro",
4822
+
"num-derive",
4823
+
"num-traits",
4824
+
"once_cell",
4825
+
"paste",
4826
+
"profiling",
4827
+
"rand 0.8.5",
4828
+
"rand_chacha 0.3.1",
4829
+
"simd_helpers",
4830
+
"system-deps",
4831
+
"thiserror 1.0.69",
4832
+
"v_frame",
4833
+
"wasm-bindgen",
4834
+
]
4835
+
4836
+
[[package]]
4837
+
name = "ravif"
4838
+
version = "0.11.12"
4839
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4840
+
checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6"
4841
+
dependencies = [
4842
+
"avif-serialize",
4843
+
"imgref",
4844
+
"loop9",
4845
+
"quick-error",
4846
+
"rav1e",
4847
+
"rayon",
4848
+
"rgb",
4849
+
]
4850
+
4851
+
[[package]]
3440
4852
name = "raw-cpuid"
3441
4853
version = "11.5.0"
3442
4854
source = "registry+https://github.com/rust-lang/crates.io-index"
3443
4855
checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146"
3444
4856
dependencies = [
3445
-
"bitflags",
4857
+
"bitflags 2.9.0",
4858
+
]
4859
+
4860
+
[[package]]
4861
+
name = "rayon"
4862
+
version = "1.10.0"
4863
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4864
+
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
4865
+
dependencies = [
4866
+
"either",
4867
+
"rayon-core",
4868
+
]
4869
+
4870
+
[[package]]
4871
+
name = "rayon-core"
4872
+
version = "1.12.1"
4873
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4874
+
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
4875
+
dependencies = [
4876
+
"crossbeam-deque",
4877
+
"crossbeam-utils",
3446
4878
]
3447
4879
3448
4880
[[package]]
···
3451
4883
source = "registry+https://github.com/rust-lang/crates.io-index"
3452
4884
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
3453
4885
dependencies = [
3454
-
"bitflags",
4886
+
"bitflags 2.9.0",
4887
+
]
4888
+
4889
+
[[package]]
4890
+
name = "ref-cast"
4891
+
version = "1.0.24"
4892
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4893
+
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
4894
+
dependencies = [
4895
+
"ref-cast-impl",
4896
+
]
4897
+
4898
+
[[package]]
4899
+
name = "ref-cast-impl"
4900
+
version = "1.0.24"
4901
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4902
+
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
4903
+
dependencies = [
4904
+
"proc-macro2",
4905
+
"quote",
4906
+
"syn 2.0.101",
3455
4907
]
3456
4908
3457
4909
[[package]]
···
3487
4939
]
3488
4940
3489
4941
[[package]]
4942
+
name = "regex-lite"
4943
+
version = "0.1.6"
4944
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4945
+
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
4946
+
4947
+
[[package]]
3490
4948
name = "regex-syntax"
3491
4949
version = "0.6.29"
3492
4950
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3500
4958
3501
4959
[[package]]
3502
4960
name = "reqwest"
4961
+
version = "0.11.27"
4962
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4963
+
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
4964
+
dependencies = [
4965
+
"base64 0.21.7",
4966
+
"bytes",
4967
+
"encoding_rs",
4968
+
"futures-core",
4969
+
"futures-util",
4970
+
"h2 0.3.26",
4971
+
"http 0.2.12",
4972
+
"http-body 0.4.6",
4973
+
"hyper 0.14.32",
4974
+
"ipnet",
4975
+
"js-sys",
4976
+
"log",
4977
+
"mime",
4978
+
"once_cell",
4979
+
"percent-encoding",
4980
+
"pin-project-lite",
4981
+
"serde",
4982
+
"serde_json",
4983
+
"serde_urlencoded",
4984
+
"sync_wrapper 0.1.2",
4985
+
"system-configuration 0.5.1",
4986
+
"tokio",
4987
+
"tower-service",
4988
+
"url",
4989
+
"wasm-bindgen",
4990
+
"wasm-bindgen-futures",
4991
+
"web-sys",
4992
+
"winreg",
4993
+
]
4994
+
4995
+
[[package]]
4996
+
name = "reqwest"
3503
4997
version = "0.12.15"
3504
4998
source = "registry+https://github.com/rust-lang/crates.io-index"
3505
4999
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
···
3508
5002
"base64 0.22.1",
3509
5003
"bytes",
3510
5004
"encoding_rs",
5005
+
"futures-channel",
3511
5006
"futures-core",
3512
5007
"futures-util",
3513
-
"h2",
5008
+
"h2 0.4.9",
3514
5009
"hickory-resolver",
3515
-
"http",
3516
-
"http-body",
5010
+
"http 1.3.1",
5011
+
"http-body 1.0.1",
3517
5012
"http-body-util",
3518
-
"hyper",
3519
-
"hyper-rustls",
5013
+
"hyper 1.6.0",
5014
+
"hyper-rustls 0.27.5",
3520
5015
"hyper-tls",
3521
5016
"hyper-util",
3522
5017
"ipnet",
···
3527
5022
"once_cell",
3528
5023
"percent-encoding",
3529
5024
"pin-project-lite",
3530
-
"rustls",
3531
-
"rustls-pemfile",
5025
+
"rustls 0.23.26",
5026
+
"rustls-pemfile 2.2.0",
3532
5027
"rustls-pki-types",
3533
5028
"serde",
3534
5029
"serde_json",
3535
5030
"serde_urlencoded",
3536
-
"sync_wrapper",
3537
-
"system-configuration",
5031
+
"sync_wrapper 1.0.2",
5032
+
"system-configuration 0.6.1",
3538
5033
"tokio",
3539
5034
"tokio-native-tls",
3540
-
"tokio-rustls",
5035
+
"tokio-rustls 0.26.2",
3541
5036
"tokio-util",
3542
5037
"tower",
3543
5038
"tower-service",
···
3558
5053
dependencies = [
3559
5054
"anyhow",
3560
5055
"async-trait",
3561
-
"http",
3562
-
"reqwest",
5056
+
"http 1.3.1",
5057
+
"reqwest 0.12.15",
3563
5058
"serde",
3564
5059
"thiserror 1.0.69",
3565
5060
"tower-service",
···
3570
5065
version = "0.7.3"
3571
5066
source = "registry+https://github.com/rust-lang/crates.io-index"
3572
5067
checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302"
5068
+
5069
+
[[package]]
5070
+
name = "rfc6979"
5071
+
version = "0.3.1"
5072
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5073
+
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
5074
+
dependencies = [
5075
+
"crypto-bigint 0.4.9",
5076
+
"hmac",
5077
+
"zeroize",
5078
+
]
3573
5079
3574
5080
[[package]]
3575
5081
name = "rfc6979"
···
3582
5088
]
3583
5089
3584
5090
[[package]]
5091
+
name = "rgb"
5092
+
version = "0.8.50"
5093
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5094
+
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
5095
+
5096
+
[[package]]
3585
5097
name = "ring"
3586
5098
version = "0.17.14"
3587
5099
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3596
5108
]
3597
5109
3598
5110
[[package]]
5111
+
name = "rocket"
5112
+
version = "0.5.1"
5113
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5114
+
checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
5115
+
dependencies = [
5116
+
"async-stream",
5117
+
"async-trait",
5118
+
"atomic 0.5.3",
5119
+
"binascii",
5120
+
"bytes",
5121
+
"either",
5122
+
"figment",
5123
+
"futures",
5124
+
"indexmap 2.9.0",
5125
+
"log",
5126
+
"memchr",
5127
+
"multer",
5128
+
"num_cpus",
5129
+
"parking_lot",
5130
+
"pin-project-lite",
5131
+
"rand 0.8.5",
5132
+
"ref-cast",
5133
+
"rocket_codegen",
5134
+
"rocket_http",
5135
+
"serde",
5136
+
"serde_json",
5137
+
"state",
5138
+
"tempfile",
5139
+
"time",
5140
+
"tokio",
5141
+
"tokio-stream",
5142
+
"tokio-util",
5143
+
"ubyte",
5144
+
"version_check",
5145
+
"yansi",
5146
+
]
5147
+
5148
+
[[package]]
5149
+
name = "rocket_codegen"
5150
+
version = "0.5.1"
5151
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5152
+
checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
5153
+
dependencies = [
5154
+
"devise",
5155
+
"glob",
5156
+
"indexmap 2.9.0",
5157
+
"proc-macro2",
5158
+
"quote",
5159
+
"rocket_http",
5160
+
"syn 2.0.101",
5161
+
"unicode-xid",
5162
+
"version_check",
5163
+
]
5164
+
5165
+
[[package]]
5166
+
name = "rocket_http"
5167
+
version = "0.5.1"
5168
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5169
+
checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
5170
+
dependencies = [
5171
+
"cookie",
5172
+
"either",
5173
+
"futures",
5174
+
"http 0.2.12",
5175
+
"hyper 0.14.32",
5176
+
"indexmap 2.9.0",
5177
+
"log",
5178
+
"memchr",
5179
+
"pear",
5180
+
"percent-encoding",
5181
+
"pin-project-lite",
5182
+
"ref-cast",
5183
+
"rustls 0.21.12",
5184
+
"rustls-pemfile 1.0.4",
5185
+
"serde",
5186
+
"smallvec",
5187
+
"stable-pattern",
5188
+
"state",
5189
+
"time",
5190
+
"tokio",
5191
+
"tokio-rustls 0.24.1",
5192
+
"uncased",
5193
+
]
5194
+
5195
+
[[package]]
5196
+
name = "rocket_sync_db_pools"
5197
+
version = "0.1.0"
5198
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5199
+
checksum = "d83f32721ed79509adac4328e97f817a8f55a47c4b64799f6fd6cc3adb6e42ff"
5200
+
dependencies = [
5201
+
"diesel",
5202
+
"r2d2",
5203
+
"rocket",
5204
+
"rocket_sync_db_pools_codegen",
5205
+
"serde",
5206
+
"tokio",
5207
+
"version_check",
5208
+
]
5209
+
5210
+
[[package]]
5211
+
name = "rocket_sync_db_pools_codegen"
5212
+
version = "0.1.0"
5213
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5214
+
checksum = "5cc890925dc79370c28eb15c9957677093fdb7e8c44966d189f38cedb995ee68"
5215
+
dependencies = [
5216
+
"devise",
5217
+
"quote",
5218
+
]
5219
+
5220
+
[[package]]
5221
+
name = "rocket_ws"
5222
+
version = "0.1.1"
5223
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5224
+
checksum = "25f1877668c937b701177c349f21383c556cd3bb4ba8fa1d07fa96ccb3a8782e"
5225
+
dependencies = [
5226
+
"rocket",
5227
+
"tokio-tungstenite 0.21.0",
5228
+
]
5229
+
5230
+
[[package]]
3599
5231
name = "rsa"
3600
5232
version = "0.9.8"
3601
5233
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3607
5239
"num-integer",
3608
5240
"num-traits",
3609
5241
"pkcs1",
3610
-
"pkcs8",
5242
+
"pkcs8 0.10.2",
3611
5243
"rand_core 0.6.4",
3612
-
"signature",
3613
-
"spki",
5244
+
"sha2",
5245
+
"signature 2.2.0",
5246
+
"spki 0.7.3",
3614
5247
"subtle",
3615
5248
"zeroize",
3616
5249
]
···
3618
5251
[[package]]
3619
5252
name = "rsky-common"
3620
5253
version = "0.1.1"
3621
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5254
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3622
5255
dependencies = [
3623
5256
"anyhow",
3624
5257
"base64ct",
···
3645
5278
[[package]]
3646
5279
name = "rsky-crypto"
3647
5280
version = "0.1.2"
3648
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5281
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3649
5282
dependencies = [
3650
5283
"anyhow",
3651
5284
"multibase",
3652
-
"p256",
5285
+
"p256 0.13.2",
3653
5286
"secp256k1",
3654
5287
"unsigned-varint 0.8.0",
3655
5288
]
···
3657
5290
[[package]]
3658
5291
name = "rsky-identity"
3659
5292
version = "0.1.0"
3660
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5293
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3661
5294
dependencies = [
3662
5295
"anyhow",
3663
5296
"hickory-resolver",
3664
-
"reqwest",
5297
+
"reqwest 0.12.15",
3665
5298
"rsky-crypto",
3666
5299
"serde",
3667
5300
"serde_json",
···
3673
5306
[[package]]
3674
5307
name = "rsky-lexicon"
3675
5308
version = "0.2.7"
3676
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5309
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3677
5310
dependencies = [
3678
5311
"anyhow",
3679
5312
"chrono",
···
3692
5325
]
3693
5326
3694
5327
[[package]]
5328
+
name = "rsky-pds"
5329
+
version = "0.1.0"
5330
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
5331
+
dependencies = [
5332
+
"anyhow",
5333
+
"argon2",
5334
+
"async-event-emitter",
5335
+
"atrium-api 0.24.10",
5336
+
"atrium-xrpc-client",
5337
+
"aws-config",
5338
+
"aws-sdk-s3",
5339
+
"base64 0.22.1",
5340
+
"base64-url",
5341
+
"base64ct",
5342
+
"chrono",
5343
+
"cid 0.10.1",
5344
+
"data-encoding",
5345
+
"diesel",
5346
+
"dotenvy",
5347
+
"email_address",
5348
+
"event-emitter-rs",
5349
+
"futures",
5350
+
"hex",
5351
+
"image",
5352
+
"indexmap 1.9.3",
5353
+
"infer 0.15.0",
5354
+
"ipld-core",
5355
+
"jwt-simple",
5356
+
"lazy_static",
5357
+
"libipld",
5358
+
"mailchecker",
5359
+
"mailgun-rs",
5360
+
"rand 0.8.5",
5361
+
"rand_core 0.6.4",
5362
+
"regex",
5363
+
"reqwest 0.12.15",
5364
+
"rocket",
5365
+
"rocket_sync_db_pools",
5366
+
"rocket_ws",
5367
+
"rsky-common",
5368
+
"rsky-crypto",
5369
+
"rsky-identity",
5370
+
"rsky-lexicon",
5371
+
"rsky-repo",
5372
+
"rsky-syntax",
5373
+
"secp256k1",
5374
+
"serde",
5375
+
"serde_bytes",
5376
+
"serde_cbor",
5377
+
"serde_derive",
5378
+
"serde_ipld_dagcbor",
5379
+
"serde_json",
5380
+
"serde_repr",
5381
+
"sha2",
5382
+
"thiserror 1.0.69",
5383
+
"time",
5384
+
"tokio",
5385
+
"toml 0.8.22",
5386
+
"tracing",
5387
+
"tracing-subscriber",
5388
+
"url",
5389
+
]
5390
+
5391
+
[[package]]
3695
5392
name = "rsky-repo"
3696
5393
version = "0.0.1"
3697
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5394
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3698
5395
dependencies = [
3699
5396
"anyhow",
3700
5397
"async-recursion",
···
3727
5424
[[package]]
3728
5425
name = "rsky-syntax"
3729
5426
version = "0.1.0"
3730
-
source = "git+https://github.com/blacksky-algorithms/rsky.git#37954845d06aaafea2b914d9096a1657abfc8d75"
5427
+
source = "git+https://github.com/blacksky-algorithms/rsky.git#e35b4c49d37df99bd2abee9307bf7d1afadc40c9"
3731
5428
dependencies = [
3732
5429
"anyhow",
3733
5430
"chrono",
···
3766
5463
source = "registry+https://github.com/rust-lang/crates.io-index"
3767
5464
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
3768
5465
dependencies = [
3769
-
"bitflags",
5466
+
"bitflags 2.9.0",
3770
5467
"errno",
3771
5468
"libc",
3772
5469
"linux-raw-sys 0.4.15",
···
3779
5476
source = "registry+https://github.com/rust-lang/crates.io-index"
3780
5477
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
3781
5478
dependencies = [
3782
-
"bitflags",
5479
+
"bitflags 2.9.0",
3783
5480
"errno",
3784
5481
"libc",
3785
5482
"linux-raw-sys 0.9.4",
···
3788
5485
3789
5486
[[package]]
3790
5487
name = "rustls"
5488
+
version = "0.21.12"
5489
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5490
+
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
5491
+
dependencies = [
5492
+
"log",
5493
+
"ring",
5494
+
"rustls-webpki 0.101.7",
5495
+
"sct",
5496
+
]
5497
+
5498
+
[[package]]
5499
+
name = "rustls"
3791
5500
version = "0.23.26"
3792
5501
source = "registry+https://github.com/rust-lang/crates.io-index"
3793
5502
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
···
3795
5504
"aws-lc-rs",
3796
5505
"once_cell",
3797
5506
"rustls-pki-types",
3798
-
"rustls-webpki",
5507
+
"rustls-webpki 0.103.1",
3799
5508
"subtle",
3800
5509
"zeroize",
3801
5510
]
3802
5511
3803
5512
[[package]]
3804
5513
name = "rustls-native-certs"
5514
+
version = "0.6.3"
5515
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5516
+
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
5517
+
dependencies = [
5518
+
"openssl-probe",
5519
+
"rustls-pemfile 1.0.4",
5520
+
"schannel",
5521
+
"security-framework 2.11.1",
5522
+
]
5523
+
5524
+
[[package]]
5525
+
name = "rustls-native-certs"
3805
5526
version = "0.8.1"
3806
5527
source = "registry+https://github.com/rust-lang/crates.io-index"
3807
5528
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
···
3814
5535
3815
5536
[[package]]
3816
5537
name = "rustls-pemfile"
5538
+
version = "1.0.4"
5539
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5540
+
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
5541
+
dependencies = [
5542
+
"base64 0.21.7",
5543
+
]
5544
+
5545
+
[[package]]
5546
+
name = "rustls-pemfile"
3817
5547
version = "2.2.0"
3818
5548
source = "registry+https://github.com/rust-lang/crates.io-index"
3819
5549
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
···
3826
5556
version = "1.11.0"
3827
5557
source = "registry+https://github.com/rust-lang/crates.io-index"
3828
5558
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
5559
+
5560
+
[[package]]
5561
+
name = "rustls-webpki"
5562
+
version = "0.101.7"
5563
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5564
+
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
5565
+
dependencies = [
5566
+
"ring",
5567
+
"untrusted",
5568
+
]
3829
5569
3830
5570
[[package]]
3831
5571
name = "rustls-webpki"
···
3861
5601
]
3862
5602
3863
5603
[[package]]
5604
+
name = "scheduled-thread-pool"
5605
+
version = "0.2.7"
5606
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5607
+
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
5608
+
dependencies = [
5609
+
"parking_lot",
5610
+
]
5611
+
5612
+
[[package]]
3864
5613
name = "scoped-tls"
3865
5614
version = "1.0.1"
3866
5615
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3871
5620
version = "1.2.0"
3872
5621
source = "registry+https://github.com/rust-lang/crates.io-index"
3873
5622
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
5623
+
5624
+
[[package]]
5625
+
name = "sct"
5626
+
version = "0.7.1"
5627
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5628
+
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
5629
+
dependencies = [
5630
+
"ring",
5631
+
"untrusted",
5632
+
]
5633
+
5634
+
[[package]]
5635
+
name = "sec1"
5636
+
version = "0.3.0"
5637
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5638
+
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
5639
+
dependencies = [
5640
+
"base16ct 0.1.1",
5641
+
"der 0.6.1",
5642
+
"generic-array",
5643
+
"pkcs8 0.9.0",
5644
+
"subtle",
5645
+
"zeroize",
5646
+
]
3874
5647
3875
5648
[[package]]
3876
5649
name = "sec1"
···
3878
5651
source = "registry+https://github.com/rust-lang/crates.io-index"
3879
5652
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
3880
5653
dependencies = [
3881
-
"base16ct",
3882
-
"der",
5654
+
"base16ct 0.2.0",
5655
+
"der 0.7.10",
3883
5656
"generic-array",
3884
-
"pkcs8",
5657
+
"pkcs8 0.10.2",
3885
5658
"subtle",
3886
5659
"zeroize",
3887
5660
]
···
3913
5686
source = "registry+https://github.com/rust-lang/crates.io-index"
3914
5687
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
3915
5688
dependencies = [
3916
-
"bitflags",
5689
+
"bitflags 2.9.0",
3917
5690
"core-foundation 0.9.4",
3918
5691
"core-foundation-sys",
3919
5692
"libc",
···
3926
5699
source = "registry+https://github.com/rust-lang/crates.io-index"
3927
5700
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
3928
5701
dependencies = [
3929
-
"bitflags",
5702
+
"bitflags 2.9.0",
3930
5703
"core-foundation 0.10.0",
3931
5704
"core-foundation-sys",
3932
5705
"libc",
···
3982
5755
source = "registry+https://github.com/rust-lang/crates.io-index"
3983
5756
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
3984
5757
dependencies = [
3985
-
"half",
5758
+
"half 1.8.3",
3986
5759
"serde",
3987
5760
]
3988
5761
···
4068
5841
]
4069
5842
4070
5843
[[package]]
5844
+
name = "serde_repr"
5845
+
version = "0.1.20"
5846
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5847
+
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
5848
+
dependencies = [
5849
+
"proc-macro2",
5850
+
"quote",
5851
+
"syn 2.0.101",
5852
+
]
5853
+
5854
+
[[package]]
4071
5855
name = "serde_spanned"
4072
5856
version = "0.6.8"
4073
5857
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4146
5930
4147
5931
[[package]]
4148
5932
name = "signature"
5933
+
version = "1.6.4"
5934
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5935
+
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
5936
+
dependencies = [
5937
+
"digest",
5938
+
"rand_core 0.6.4",
5939
+
]
5940
+
5941
+
[[package]]
5942
+
name = "signature"
4149
5943
version = "2.2.0"
4150
5944
source = "registry+https://github.com/rust-lang/crates.io-index"
4151
5945
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
···
4155
5949
]
4156
5950
4157
5951
[[package]]
5952
+
name = "simd-adler32"
5953
+
version = "0.3.7"
5954
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5955
+
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
5956
+
5957
+
[[package]]
5958
+
name = "simd_helpers"
5959
+
version = "0.1.0"
5960
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5961
+
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
5962
+
dependencies = [
5963
+
"quote",
5964
+
]
5965
+
5966
+
[[package]]
4158
5967
name = "sketches-ddsketch"
4159
5968
version = "0.3.0"
4160
5969
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4174
5983
version = "1.15.0"
4175
5984
source = "registry+https://github.com/rust-lang/crates.io-index"
4176
5985
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
4177
-
dependencies = [
4178
-
"serde",
4179
-
]
4180
5986
4181
5987
[[package]]
4182
5988
name = "socket2"
···
4193
5999
version = "0.9.8"
4194
6000
source = "registry+https://github.com/rust-lang/crates.io-index"
4195
6001
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
4196
-
dependencies = [
4197
-
"lock_api",
4198
-
]
4199
6002
4200
6003
[[package]]
4201
6004
name = "spki"
4202
-
version = "0.7.3"
6005
+
version = "0.6.0"
4203
6006
source = "registry+https://github.com/rust-lang/crates.io-index"
4204
-
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
6007
+
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
4205
6008
dependencies = [
4206
6009
"base64ct",
4207
-
"der",
4208
-
]
4209
-
4210
-
[[package]]
4211
-
name = "sqlx"
4212
-
version = "0.8.5"
4213
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4214
-
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
4215
-
dependencies = [
4216
-
"sqlx-core",
4217
-
"sqlx-macros",
4218
-
"sqlx-mysql",
4219
-
"sqlx-postgres",
4220
-
"sqlx-sqlite",
4221
-
]
4222
-
4223
-
[[package]]
4224
-
name = "sqlx-core"
4225
-
version = "0.8.5"
4226
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4227
-
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
4228
-
dependencies = [
4229
-
"base64 0.22.1",
4230
-
"bytes",
4231
-
"crc",
4232
-
"crossbeam-queue",
4233
-
"either",
4234
-
"event-listener 5.4.0",
4235
-
"futures-core",
4236
-
"futures-intrusive",
4237
-
"futures-io",
4238
-
"futures-util",
4239
-
"hashbrown 0.15.3",
4240
-
"hashlink",
4241
-
"indexmap 2.9.0",
4242
-
"log",
4243
-
"memchr",
4244
-
"once_cell",
4245
-
"percent-encoding",
4246
-
"serde",
4247
-
"serde_json",
4248
-
"sha2",
4249
-
"smallvec",
4250
-
"thiserror 2.0.12",
4251
-
"tokio",
4252
-
"tokio-stream",
4253
-
"tracing",
4254
-
"url",
6010
+
"der 0.6.1",
4255
6011
]
4256
6012
4257
6013
[[package]]
4258
-
name = "sqlx-macros"
4259
-
version = "0.8.5"
4260
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4261
-
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
4262
-
dependencies = [
4263
-
"proc-macro2",
4264
-
"quote",
4265
-
"sqlx-core",
4266
-
"sqlx-macros-core",
4267
-
"syn 2.0.101",
4268
-
]
4269
-
4270
-
[[package]]
4271
-
name = "sqlx-macros-core"
4272
-
version = "0.8.5"
6014
+
name = "spki"
6015
+
version = "0.7.3"
4273
6016
source = "registry+https://github.com/rust-lang/crates.io-index"
4274
-
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
6017
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
4275
6018
dependencies = [
4276
-
"dotenvy",
4277
-
"either",
4278
-
"heck",
4279
-
"hex",
4280
-
"once_cell",
4281
-
"proc-macro2",
4282
-
"quote",
4283
-
"serde",
4284
-
"serde_json",
4285
-
"sha2",
4286
-
"sqlx-core",
4287
-
"sqlx-mysql",
4288
-
"sqlx-postgres",
4289
-
"sqlx-sqlite",
4290
-
"syn 2.0.101",
4291
-
"tempfile",
4292
-
"tokio",
4293
-
"url",
6019
+
"base64ct",
6020
+
"der 0.7.10",
4294
6021
]
4295
6022
4296
6023
[[package]]
4297
-
name = "sqlx-mysql"
4298
-
version = "0.8.5"
6024
+
name = "stable-pattern"
6025
+
version = "0.1.0"
4299
6026
source = "registry+https://github.com/rust-lang/crates.io-index"
4300
-
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
6027
+
checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
4301
6028
dependencies = [
4302
-
"atoi",
4303
-
"base64 0.22.1",
4304
-
"bitflags",
4305
-
"byteorder",
4306
-
"bytes",
4307
-
"crc",
4308
-
"digest",
4309
-
"dotenvy",
4310
-
"either",
4311
-
"futures-channel",
4312
-
"futures-core",
4313
-
"futures-io",
4314
-
"futures-util",
4315
-
"generic-array",
4316
-
"hex",
4317
-
"hkdf",
4318
-
"hmac",
4319
-
"itoa",
4320
-
"log",
4321
-
"md-5",
4322
6029
"memchr",
4323
-
"once_cell",
4324
-
"percent-encoding",
4325
-
"rand 0.8.5",
4326
-
"rsa",
4327
-
"serde",
4328
-
"sha1",
4329
-
"sha2",
4330
-
"smallvec",
4331
-
"sqlx-core",
4332
-
"stringprep",
4333
-
"thiserror 2.0.12",
4334
-
"tracing",
4335
-
"whoami",
4336
-
]
4337
-
4338
-
[[package]]
4339
-
name = "sqlx-postgres"
4340
-
version = "0.8.5"
4341
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4342
-
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
4343
-
dependencies = [
4344
-
"atoi",
4345
-
"base64 0.22.1",
4346
-
"bitflags",
4347
-
"byteorder",
4348
-
"crc",
4349
-
"dotenvy",
4350
-
"etcetera",
4351
-
"futures-channel",
4352
-
"futures-core",
4353
-
"futures-util",
4354
-
"hex",
4355
-
"hkdf",
4356
-
"hmac",
4357
-
"home",
4358
-
"itoa",
4359
-
"log",
4360
-
"md-5",
4361
-
"memchr",
4362
-
"once_cell",
4363
-
"rand 0.8.5",
4364
-
"serde",
4365
-
"serde_json",
4366
-
"sha2",
4367
-
"smallvec",
4368
-
"sqlx-core",
4369
-
"stringprep",
4370
-
"thiserror 2.0.12",
4371
-
"tracing",
4372
-
"whoami",
4373
-
]
4374
-
4375
-
[[package]]
4376
-
name = "sqlx-sqlite"
4377
-
version = "0.8.5"
4378
-
source = "registry+https://github.com/rust-lang/crates.io-index"
4379
-
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
4380
-
dependencies = [
4381
-
"atoi",
4382
-
"flume",
4383
-
"futures-channel",
4384
-
"futures-core",
4385
-
"futures-executor",
4386
-
"futures-intrusive",
4387
-
"futures-util",
4388
-
"libsqlite3-sys",
4389
-
"log",
4390
-
"percent-encoding",
4391
-
"serde",
4392
-
"serde_urlencoded",
4393
-
"sqlx-core",
4394
-
"thiserror 2.0.12",
4395
-
"tracing",
4396
-
"url",
4397
6030
]
4398
6031
4399
6032
[[package]]
···
4403
6036
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
4404
6037
4405
6038
[[package]]
4406
-
name = "stringprep"
4407
-
version = "0.1.5"
6039
+
name = "state"
6040
+
version = "0.6.0"
4408
6041
source = "registry+https://github.com/rust-lang/crates.io-index"
4409
-
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
6042
+
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
4410
6043
dependencies = [
4411
-
"unicode-bidi",
4412
-
"unicode-normalization",
4413
-
"unicode-properties",
6044
+
"loom 0.5.6",
4414
6045
]
4415
6046
4416
6047
[[package]]
···
4432
6063
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
4433
6064
4434
6065
[[package]]
6066
+
name = "superboring"
6067
+
version = "0.1.4"
6068
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6069
+
checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f"
6070
+
dependencies = [
6071
+
"getrandom 0.2.16",
6072
+
"hmac-sha256",
6073
+
"hmac-sha512",
6074
+
"rand 0.8.5",
6075
+
"rsa",
6076
+
]
6077
+
6078
+
[[package]]
4435
6079
name = "syn"
4436
6080
version = "1.0.109"
4437
6081
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4452
6096
"quote",
4453
6097
"unicode-ident",
4454
6098
]
6099
+
6100
+
[[package]]
6101
+
name = "sync_wrapper"
6102
+
version = "0.1.2"
6103
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6104
+
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
4455
6105
4456
6106
[[package]]
4457
6107
name = "sync_wrapper"
···
4487
6137
4488
6138
[[package]]
4489
6139
name = "system-configuration"
6140
+
version = "0.5.1"
6141
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6142
+
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
6143
+
dependencies = [
6144
+
"bitflags 1.3.2",
6145
+
"core-foundation 0.9.4",
6146
+
"system-configuration-sys 0.5.0",
6147
+
]
6148
+
6149
+
[[package]]
6150
+
name = "system-configuration"
4490
6151
version = "0.6.1"
4491
6152
source = "registry+https://github.com/rust-lang/crates.io-index"
4492
6153
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
4493
6154
dependencies = [
4494
-
"bitflags",
6155
+
"bitflags 2.9.0",
4495
6156
"core-foundation 0.9.4",
4496
-
"system-configuration-sys",
6157
+
"system-configuration-sys 0.6.0",
6158
+
]
6159
+
6160
+
[[package]]
6161
+
name = "system-configuration-sys"
6162
+
version = "0.5.0"
6163
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6164
+
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
6165
+
dependencies = [
6166
+
"core-foundation-sys",
6167
+
"libc",
4497
6168
]
4498
6169
4499
6170
[[package]]
···
4507
6178
]
4508
6179
4509
6180
[[package]]
6181
+
name = "system-deps"
6182
+
version = "6.2.2"
6183
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6184
+
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
6185
+
dependencies = [
6186
+
"cfg-expr",
6187
+
"heck",
6188
+
"pkg-config",
6189
+
"toml 0.8.22",
6190
+
"version-compare",
6191
+
]
6192
+
6193
+
[[package]]
4510
6194
name = "tagptr"
4511
6195
version = "0.2.0"
4512
6196
source = "registry+https://github.com/rust-lang/crates.io-index"
4513
6197
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
6198
+
6199
+
[[package]]
6200
+
name = "target-lexicon"
6201
+
version = "0.12.16"
6202
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6203
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
4514
6204
4515
6205
[[package]]
4516
6206
name = "tempfile"
···
4573
6263
dependencies = [
4574
6264
"cfg-if",
4575
6265
"once_cell",
6266
+
]
6267
+
6268
+
[[package]]
6269
+
name = "tiff"
6270
+
version = "0.9.1"
6271
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6272
+
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
6273
+
dependencies = [
6274
+
"flate2",
6275
+
"jpeg-decoder",
6276
+
"weezl",
4576
6277
]
4577
6278
4578
6279
[[package]]
···
4675
6376
4676
6377
[[package]]
4677
6378
name = "tokio-rustls"
6379
+
version = "0.24.1"
6380
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6381
+
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
6382
+
dependencies = [
6383
+
"rustls 0.21.12",
6384
+
"tokio",
6385
+
]
6386
+
6387
+
[[package]]
6388
+
name = "tokio-rustls"
4678
6389
version = "0.26.2"
4679
6390
source = "registry+https://github.com/rust-lang/crates.io-index"
4680
6391
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
4681
6392
dependencies = [
4682
-
"rustls",
6393
+
"rustls 0.23.26",
4683
6394
"tokio",
4684
6395
]
4685
6396
···
4696
6407
4697
6408
[[package]]
4698
6409
name = "tokio-tungstenite"
6410
+
version = "0.21.0"
6411
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6412
+
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
6413
+
dependencies = [
6414
+
"futures-util",
6415
+
"log",
6416
+
"tokio",
6417
+
"tungstenite 0.21.0",
6418
+
]
6419
+
6420
+
[[package]]
6421
+
name = "tokio-tungstenite"
4699
6422
version = "0.26.2"
4700
6423
source = "registry+https://github.com/rust-lang/crates.io-index"
4701
6424
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
···
4703
6426
"futures-util",
4704
6427
"log",
4705
6428
"tokio",
4706
-
"tungstenite",
6429
+
"tungstenite 0.26.2",
4707
6430
]
4708
6431
4709
6432
[[package]]
···
4731
6454
4732
6455
[[package]]
4733
6456
name = "toml"
6457
+
version = "0.7.8"
6458
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6459
+
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
6460
+
dependencies = [
6461
+
"serde",
6462
+
"serde_spanned",
6463
+
"toml_datetime",
6464
+
"toml_edit 0.19.15",
6465
+
]
6466
+
6467
+
[[package]]
6468
+
name = "toml"
4734
6469
version = "0.8.22"
4735
6470
source = "registry+https://github.com/rust-lang/crates.io-index"
4736
6471
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
···
4738
6473
"serde",
4739
6474
"serde_spanned",
4740
6475
"toml_datetime",
4741
-
"toml_edit",
6476
+
"toml_edit 0.22.26",
4742
6477
]
4743
6478
4744
6479
[[package]]
···
4752
6487
4753
6488
[[package]]
4754
6489
name = "toml_edit"
6490
+
version = "0.19.15"
6491
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6492
+
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
6493
+
dependencies = [
6494
+
"indexmap 2.9.0",
6495
+
"serde",
6496
+
"serde_spanned",
6497
+
"toml_datetime",
6498
+
"winnow 0.5.40",
6499
+
]
6500
+
6501
+
[[package]]
6502
+
name = "toml_edit"
4755
6503
version = "0.22.26"
4756
6504
source = "registry+https://github.com/rust-lang/crates.io-index"
4757
6505
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
···
4761
6509
"serde_spanned",
4762
6510
"toml_datetime",
4763
6511
"toml_write",
4764
-
"winnow",
6512
+
"winnow 0.7.9",
4765
6513
]
4766
6514
4767
6515
[[package]]
···
4779
6527
"futures-core",
4780
6528
"futures-util",
4781
6529
"pin-project-lite",
4782
-
"sync_wrapper",
6530
+
"sync_wrapper 1.0.2",
4783
6531
"tokio",
4784
6532
"tower-layer",
4785
6533
"tower-service",
···
4792
6540
source = "registry+https://github.com/rust-lang/crates.io-index"
4793
6541
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
4794
6542
dependencies = [
4795
-
"bitflags",
6543
+
"bitflags 2.9.0",
4796
6544
"bytes",
4797
6545
"futures-util",
4798
-
"http",
4799
-
"http-body",
6546
+
"http 1.3.1",
6547
+
"http-body 1.0.1",
4800
6548
"http-body-util",
4801
6549
"http-range-header",
4802
6550
"httpdate",
···
4904
6652
4905
6653
[[package]]
4906
6654
name = "tungstenite"
6655
+
version = "0.21.0"
6656
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6657
+
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
6658
+
dependencies = [
6659
+
"byteorder",
6660
+
"bytes",
6661
+
"data-encoding",
6662
+
"http 1.3.1",
6663
+
"httparse",
6664
+
"log",
6665
+
"rand 0.8.5",
6666
+
"sha1",
6667
+
"thiserror 1.0.69",
6668
+
"url",
6669
+
"utf-8",
6670
+
]
6671
+
6672
+
[[package]]
6673
+
name = "tungstenite"
4907
6674
version = "0.26.2"
4908
6675
source = "registry+https://github.com/rust-lang/crates.io-index"
4909
6676
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
4910
6677
dependencies = [
4911
6678
"bytes",
4912
6679
"data-encoding",
4913
-
"http",
6680
+
"http 1.3.1",
4914
6681
"httparse",
4915
6682
"log",
4916
6683
"rand 0.9.1",
4917
6684
"sha1",
4918
6685
"thiserror 2.0.12",
4919
6686
"utf-8",
6687
+
]
6688
+
6689
+
[[package]]
6690
+
name = "typed-builder"
6691
+
version = "0.15.2"
6692
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6693
+
checksum = "7fe83c85a85875e8c4cb9ce4a890f05b23d38cd0d47647db7895d3d2a79566d2"
6694
+
dependencies = [
6695
+
"typed-builder-macro",
6696
+
]
6697
+
6698
+
[[package]]
6699
+
name = "typed-builder-macro"
6700
+
version = "0.15.2"
6701
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6702
+
checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2"
6703
+
dependencies = [
6704
+
"proc-macro2",
6705
+
"quote",
6706
+
"syn 2.0.101",
4920
6707
]
4921
6708
4922
6709
[[package]]
···
4952
6739
"http-types",
4953
6740
"pin-project",
4954
6741
"rand 0.8.5",
4955
-
"reqwest",
6742
+
"reqwest 0.12.15",
4956
6743
"serde",
4957
6744
"serde_json",
4958
6745
"time",
···
4961
6748
"typespec",
4962
6749
"typespec_macros",
4963
6750
"url",
4964
-
"uuid",
6751
+
"uuid 1.16.0",
4965
6752
]
4966
6753
4967
6754
[[package]]
···
4985
6772
]
4986
6773
4987
6774
[[package]]
6775
+
name = "ubyte"
6776
+
version = "0.10.4"
6777
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6778
+
checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
6779
+
dependencies = [
6780
+
"serde",
6781
+
]
6782
+
6783
+
[[package]]
4988
6784
name = "uncased"
4989
6785
version = "0.9.10"
4990
6786
source = "registry+https://github.com/rust-lang/crates.io-index"
4991
6787
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
4992
6788
dependencies = [
6789
+
"serde",
4993
6790
"version_check",
4994
6791
]
4995
6792
···
5000
6797
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
5001
6798
5002
6799
[[package]]
5003
-
name = "unicode-bidi"
5004
-
version = "0.3.18"
5005
-
source = "registry+https://github.com/rust-lang/crates.io-index"
5006
-
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
5007
-
5008
-
[[package]]
5009
6800
name = "unicode-ident"
5010
6801
version = "1.0.18"
5011
6802
source = "registry+https://github.com/rust-lang/crates.io-index"
5012
6803
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
5013
6804
5014
6805
[[package]]
5015
-
name = "unicode-normalization"
5016
-
version = "0.1.24"
5017
-
source = "registry+https://github.com/rust-lang/crates.io-index"
5018
-
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
5019
-
dependencies = [
5020
-
"tinyvec",
5021
-
]
5022
-
5023
-
[[package]]
5024
-
name = "unicode-properties"
5025
-
version = "0.1.3"
5026
-
source = "registry+https://github.com/rust-lang/crates.io-index"
5027
-
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
5028
-
5029
-
[[package]]
5030
6806
name = "unicode-width"
5031
6807
version = "0.1.14"
5032
6808
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5104
6880
5105
6881
[[package]]
5106
6882
name = "uuid"
6883
+
version = "0.8.2"
6884
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6885
+
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
6886
+
dependencies = [
6887
+
"getrandom 0.2.16",
6888
+
]
6889
+
6890
+
[[package]]
6891
+
name = "uuid"
5107
6892
version = "1.16.0"
5108
6893
source = "registry+https://github.com/rust-lang/crates.io-index"
5109
6894
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
···
5112
6897
]
5113
6898
5114
6899
[[package]]
6900
+
name = "v_frame"
6901
+
version = "0.3.8"
6902
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6903
+
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
6904
+
dependencies = [
6905
+
"aligned-vec",
6906
+
"num-traits",
6907
+
"wasm-bindgen",
6908
+
]
6909
+
6910
+
[[package]]
5115
6911
name = "valuable"
5116
6912
version = "0.1.1"
5117
6913
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5124
6920
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
5125
6921
5126
6922
[[package]]
6923
+
name = "version-compare"
6924
+
version = "0.2.0"
6925
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6926
+
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
6927
+
6928
+
[[package]]
5127
6929
name = "version_check"
5128
6930
version = "0.9.5"
5129
6931
source = "registry+https://github.com/rust-lang/crates.io-index"
5130
6932
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
5131
6933
5132
6934
[[package]]
6935
+
name = "vsimd"
6936
+
version = "0.8.0"
6937
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6938
+
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
6939
+
6940
+
[[package]]
5133
6941
name = "waker-fn"
5134
6942
version = "1.2.0"
5135
6943
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5166
6974
]
5167
6975
5168
6976
[[package]]
5169
-
name = "wasite"
5170
-
version = "0.1.0"
6977
+
name = "wasix"
6978
+
version = "0.12.21"
5171
6979
source = "registry+https://github.com/rust-lang/crates.io-index"
5172
-
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
6980
+
checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d"
6981
+
dependencies = [
6982
+
"wasi 0.11.0+wasi-snapshot-preview1",
6983
+
]
5173
6984
5174
6985
[[package]]
5175
6986
name = "wasm-bindgen"
···
5294
7105
]
5295
7106
5296
7107
[[package]]
7108
+
name = "weezl"
7109
+
version = "0.1.8"
7110
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7111
+
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
7112
+
7113
+
[[package]]
5297
7114
name = "which"
5298
7115
version = "4.4.2"
5299
7116
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5303
7120
"home",
5304
7121
"once_cell",
5305
7122
"rustix 0.38.44",
5306
-
]
5307
-
5308
-
[[package]]
5309
-
name = "whoami"
5310
-
version = "1.6.0"
5311
-
source = "registry+https://github.com/rust-lang/crates.io-index"
5312
-
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
5313
-
dependencies = [
5314
-
"redox_syscall",
5315
-
"wasite",
5316
7123
]
5317
7124
5318
7125
[[package]]
···
5342
7149
version = "0.4.0"
5343
7150
source = "registry+https://github.com/rust-lang/crates.io-index"
5344
7151
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
7152
+
7153
+
[[package]]
7154
+
name = "windows"
7155
+
version = "0.48.0"
7156
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7157
+
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
7158
+
dependencies = [
7159
+
"windows-targets 0.48.5",
7160
+
]
5345
7161
5346
7162
[[package]]
5347
7163
name = "windows"
···
5656
7472
5657
7473
[[package]]
5658
7474
name = "winnow"
7475
+
version = "0.5.40"
7476
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7477
+
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
7478
+
dependencies = [
7479
+
"memchr",
7480
+
]
7481
+
7482
+
[[package]]
7483
+
name = "winnow"
5659
7484
version = "0.7.9"
5660
7485
source = "registry+https://github.com/rust-lang/crates.io-index"
5661
7486
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
···
5679
7504
source = "registry+https://github.com/rust-lang/crates.io-index"
5680
7505
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
5681
7506
dependencies = [
5682
-
"bitflags",
7507
+
"bitflags 2.9.0",
5683
7508
]
5684
7509
5685
7510
[[package]]
···
5695
7520
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
5696
7521
5697
7522
[[package]]
7523
+
name = "xmlparser"
7524
+
version = "0.13.6"
7525
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7526
+
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
7527
+
7528
+
[[package]]
5698
7529
name = "yansi"
5699
7530
version = "1.0.1"
5700
7531
source = "registry+https://github.com/rust-lang/crates.io-index"
5701
7532
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
7533
+
dependencies = [
7534
+
"is-terminal",
7535
+
]
5702
7536
5703
7537
[[package]]
5704
7538
name = "yoke"
···
5812
7646
"quote",
5813
7647
"syn 2.0.101",
5814
7648
]
7649
+
7650
+
[[package]]
7651
+
name = "zune-core"
7652
+
version = "0.4.12"
7653
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7654
+
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
7655
+
7656
+
[[package]]
7657
+
name = "zune-inflate"
7658
+
version = "0.2.54"
7659
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7660
+
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
7661
+
dependencies = [
7662
+
"simd-adler32",
7663
+
]
7664
+
7665
+
[[package]]
7666
+
name = "zune-jpeg"
7667
+
version = "0.4.14"
7668
+
source = "registry+https://github.com/rust-lang/crates.io-index"
7669
+
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
7670
+
dependencies = [
7671
+
"zune-core",
7672
+
]
+86
-24
Cargo.toml
+86
-24
Cargo.toml
···
128
128
# expect_used = "deny"
129
129
130
130
[dependencies]
131
+
multihash = "0.19.3"
132
+
diesel = { version = "2.1.5", features = ["chrono", "sqlite", "r2d2"] }
133
+
diesel_migrations = { version = "2.1.0" }
134
+
r2d2 = "0.8.10"
135
+
136
+
atrium-repo = "0.1"
131
137
atrium-api = "0.25"
138
+
# atrium-common = { version = "0.1.2", path = "atrium-common" }
132
139
atrium-crypto = "0.1"
133
-
atrium-repo = "0.1"
140
+
# atrium-identity = { version = "0.1.4", path = "atrium-identity" }
134
141
atrium-xrpc = "0.12"
135
142
atrium-xrpc-client = "0.5"
143
+
# bsky-sdk = { version = "0.1.19", path = "bsky-sdk" }
144
+
rsky-syntax = { git = "https://github.com/blacksky-algorithms/rsky.git" }
145
+
rsky-repo = { git = "https://github.com/blacksky-algorithms/rsky.git" }
146
+
rsky-pds = { git = "https://github.com/blacksky-algorithms/rsky.git" }
147
+
rsky-common = { git = "https://github.com/blacksky-algorithms/rsky.git" }
136
148
149
+
# async in streams
150
+
# async-stream = "0.3"
151
+
152
+
# DAG-CBOR codec
153
+
ipld-core = "0.4.2"
154
+
serde_ipld_dagcbor = { version = "0.6.2", default-features = false, features = [
155
+
"std",
156
+
] }
157
+
serde_ipld_dagjson = "0.2.0"
158
+
cidv10 = { version = "0.10.1", package = "cid" }
159
+
160
+
# Parsing and validation
161
+
base64 = "0.22.1"
162
+
chrono = "0.4.39"
163
+
hex = "0.4.3"
164
+
# langtag = "0.3"
165
+
# multibase = "0.9.1"
166
+
regex = "1.11.1"
167
+
serde = { version = "1.0.218", features = ["derive"] }
168
+
serde_bytes = "0.11.17"
169
+
# serde_html_form = "0.2.6"
170
+
serde_json = "1.0.139"
171
+
# unsigned-varint = "0.8"
172
+
173
+
# Cryptography
174
+
# ecdsa = "0.16.9"
175
+
# elliptic-curve = "0.13.6"
176
+
# jose-jwa = "0.1.2"
177
+
# jose-jwk = { version = "0.1.2", default-features = false }
178
+
k256 = "0.13.4"
179
+
# p256 = { version = "0.13.2", default-features = false }
180
+
rand = "0.8.5"
181
+
sha2 = "0.10.8"
182
+
183
+
# Networking
184
+
# dashmap = "6.1.0"
185
+
futures = "0.3.31"
186
+
# hickory-proto = { version = "0.24.3", default-features = false }
187
+
# hickory-resolver = "0.24.1"
188
+
# http = "1.1.0"
189
+
# lru = "0.12.4"
190
+
# moka = "0.12.8"
191
+
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
192
+
tokio-util = { version = "0.7.13", features = ["io"] }
193
+
194
+
# HTTP client integrations
195
+
# isahc = "1.7.2"
196
+
reqwest = { version = "0.12.12", features = ["json"] }
197
+
198
+
# Errors
137
199
anyhow = "1.0.96"
200
+
thiserror = "2.0.11"
201
+
202
+
# CLI
203
+
clap = { version = "4.5.30", features = ["derive"] }
204
+
# dirs = "5.0.1"
205
+
206
+
# Testing
207
+
# gloo-timers = { version = "0.3.0", features = ["futures"] }
208
+
# mockito = "=1.6.1"
209
+
210
+
# WebAssembly
211
+
# wasm-bindgen-test = "0.3.41"
212
+
# web-time = "1.1.0"
213
+
# bumpalo = "~3.14.0"
214
+
215
+
# Code generation
216
+
# trait-variant = "0.1.2"
217
+
218
+
# Others
219
+
# base64ct = "=1.6.0"
220
+
# litemap = "=0.7.4"
221
+
# native-tls = "=0.2.13"
222
+
# zerofrom = "=0.1.5"
138
223
argon2 = { version = "0.5.3", features = ["std"] }
139
224
axum = { version = "0.8.1", features = [
140
225
"tower-log",
···
146
231
azure_core = "0.22.0"
147
232
azure_identity = "0.22.0"
148
233
base32 = "0.5.1"
149
-
base64 = "0.22.1"
150
-
chrono = "0.4.39"
151
-
clap = { version = "4.5.30", features = ["derive"] }
152
234
clap-verbosity-flag = "3.0.2"
153
235
constcat = "0.6.0"
154
236
figment = { version = "0.10.19", features = ["toml", "env"] }
155
-
futures = "0.3.31"
156
237
http-cache-reqwest = { version = "0.15.1", default-features = false, features = [
157
238
"manager-moka",
158
239
] }
159
240
memmap2 = "0.9.5"
160
241
metrics = "0.24.1"
161
242
metrics-exporter-prometheus = "0.16.2"
162
-
rand = "0.8.5"
163
-
reqwest = { version = "0.12.12", features = ["json"] }
164
243
reqwest-middleware = { version = "0.4.0", features = ["json"] }
165
-
serde = { version = "1.0.218", features = ["derive"] }
166
-
serde_ipld_dagcbor = { version = "0.6.2", default-features = false, features = ["std"] }
167
-
serde_json = "1.0.139"
168
-
sha2 = "0.10.8"
169
-
sqlx = { version = "0.8.3", features = ["json", "runtime-tokio", "sqlite"] }
170
-
thiserror = "2.0.11"
171
-
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
172
-
tokio-util = { version = "0.7.13", features = ["io"] }
173
244
tower-http = { version = "0.6.2", features = ["cors", "fs", "trace"] }
174
245
tracing = "0.1.41"
175
246
tracing-subscriber = "0.3.19"
···
177
248
uuid = { version = "1.14.0", features = ["v4"] }
178
249
urlencoding = "2.1.3"
179
250
async-trait = "0.1.88"
180
-
k256 = "0.13.4"
181
-
hex = "0.4.3"
182
-
ipld-core = "0.4.2"
183
251
lazy_static = "1.5.0"
184
-
regex = "1.11.1"
185
-
rsky-syntax = { git = "https://github.com/blacksky-algorithms/rsky.git" }
186
-
rsky-repo = { git = "https://github.com/blacksky-algorithms/rsky.git" }
187
-
serde_bytes = "0.11.17"
188
-
multihash = "0.19.3"
189
-
serde_ipld_dagjson = "0.2.0"
+1
-1
src/actor_store/actor_store.rs
+1
-1
src/actor_store/actor_store.rs
···
5
5
6
6
use super::ActorDb;
7
7
use super::actor_store_reader::ActorStoreReader;
8
+
use super::actor_store_resources::ActorStoreResources;
8
9
use super::actor_store_transactor::ActorStoreTransactor;
9
10
use super::actor_store_writer::ActorStoreWriter;
10
-
use super::resources::ActorStoreResources;
11
11
use crate::SigningKey;
12
12
13
13
pub(crate) struct ActorStore {
+15
-14
src/actor_store/actor_store_reader.rs
+15
-14
src/actor_store/actor_store_reader.rs
···
1
1
use anyhow::Result;
2
+
use diesel::prelude::*;
2
3
use std::sync::Arc;
3
4
4
5
use super::{
···
20
21
/// Function to get keypair.
21
22
keypair_fn: Box<dyn Fn() -> Result<Arc<SigningKey>> + Send + Sync>,
22
23
/// Database connection.
23
-
db: ActorDb,
24
+
pub(crate) db: ActorDb,
24
25
/// Actor store resources.
25
26
resources: ActorStoreResources,
26
27
}
···
34
35
keypair: impl Fn() -> Result<Arc<SigningKey>> + Send + Sync + 'static,
35
36
) -> Self {
36
37
// Create readers
37
-
let record = RecordReader::new(db.clone());
38
-
let pref = PreferenceReader::new(db.clone());
38
+
let record = RecordReader::new(db.clone(), did.clone());
39
+
let pref = PreferenceReader::new(db.clone(), did.clone());
39
40
40
41
// Store keypair function for later use
41
42
let keypair_fn = Box::new(keypair);
···
44
45
let _ = keypair_fn();
45
46
46
47
// Create repo reader
47
-
let repo = RepoReader::new(db.clone(), resources.blobstore(did.clone()));
48
+
let repo = RepoReader::new(db.clone(), resources.blobstore(did.clone()), did.clone());
48
49
49
50
Self {
50
51
did,
···
65
66
/// Execute a transaction with the actor store.
66
67
pub(crate) async fn transact<T, F>(&self, f: F) -> Result<T>
67
68
where
68
-
F: FnOnce(ActorStoreTransactor) -> Result<T>,
69
+
F: FnOnce(ActorStoreTransactor) -> Result<T> + Send,
70
+
T: Send + 'static,
69
71
{
70
72
let keypair = self.keypair()?;
71
73
let did = self.did.clone();
72
74
let resources = self.resources.clone();
73
75
74
76
self.db
75
-
.transaction_no_retry(move |tx| {
77
+
.transaction(move |conn| {
76
78
// Create a transactor with the transaction
77
-
let store = ActorStoreTransactor::new_with_transaction(
78
-
did,
79
-
tx, // Pass the transaction directly
80
-
keypair.clone(),
81
-
resources,
82
-
);
79
+
// We'll create a temporary ActorDb with the same pool
80
+
let db = ActorDb {
81
+
pool: self.db.pool.clone(),
82
+
};
83
+
84
+
let store = ActorStoreTransactor::new(did, db, keypair.clone(), resources);
83
85
84
86
// Execute user function
85
-
f(store).map_err(|e| sqlx::Error::Custom(Box::new(e))) // Convert anyhow::Error to sqlx::Error
87
+
f(store)
86
88
})
87
89
.await
88
-
.map_err(|e| anyhow::anyhow!("Transaction error: {:?}", e))
89
90
}
90
91
}
+4
-6
src/actor_store/actor_store_resources.rs
+4
-6
src/actor_store/actor_store_resources.rs
···
1
-
use crate::repo::types::BlobStore;
2
-
3
-
use super::blob::BackgroundQueue;
1
+
use super::blob::{BackgroundQueue, BlobStorePlaceholder};
4
2
pub(crate) struct ActorStoreResources {
5
-
pub(crate) blobstore: fn(did: String) -> BlobStore,
3
+
pub(crate) blobstore: fn(did: String) -> BlobStorePlaceholder,
6
4
pub(crate) background_queue: BackgroundQueue,
7
5
pub(crate) reserved_key_dir: Option<String>,
8
6
}
9
7
impl ActorStoreResources {
10
8
pub(crate) fn new(
11
-
blobstore: fn(did: String) -> BlobStore,
9
+
blobstore: fn(did: String) -> BlobStorePlaceholder,
12
10
background_queue: BackgroundQueue,
13
11
reserved_key_dir: Option<String>,
14
12
) -> Self {
···
19
17
}
20
18
}
21
19
22
-
pub(crate) fn blobstore(&self, did: String) -> BlobStore {
20
+
pub(crate) fn blobstore(&self, did: String) -> BlobStorePlaceholder {
23
21
(self.blobstore)(did)
24
22
}
25
23
}
+20
-5
src/actor_store/blob/background.rs
+20
-5
src/actor_store/blob/background.rs
···
1
+
use std::future::Future;
1
2
use std::sync::Arc;
2
3
use tokio::sync::{Mutex, Semaphore};
3
4
use tokio::task::{self, JoinHandle};
4
5
use tracing::error;
5
6
6
-
/// Background Queue
7
+
/// Background Queue for asynchronous processing tasks
8
+
///
7
9
/// A simple queue for in-process, out-of-band/backgrounded work
10
+
#[derive(Clone)]
8
11
pub struct BackgroundQueue {
9
12
semaphore: Arc<Semaphore>,
10
13
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
···
22
25
}
23
26
24
27
/// Add a task to the queue
25
-
pub async fn add<F>(&self, fut: F)
28
+
pub async fn add<F>(&self, future: F)
26
29
where
27
30
F: Future<Output = ()> + Send + 'static,
28
31
{
···
31
34
return;
32
35
}
33
36
34
-
let permit = self.semaphore.clone().acquire_owned().await.unwrap();
37
+
let permit = match self.semaphore.clone().acquire_owned().await {
38
+
Ok(p) => p,
39
+
Err(_) => {
40
+
error!("Failed to acquire semaphore permit for background task");
41
+
return;
42
+
}
43
+
};
44
+
35
45
let tasks = self.tasks.clone();
36
46
37
47
let handle = task::spawn(async move {
38
-
_ = fut.await;
48
+
future.await;
49
+
50
+
// Catch any panics to prevent task failures from propagating
39
51
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {})) {
40
-
error!("background queue task panicked: {:?}", e);
52
+
error!("Background queue task panicked: {:?}", e);
41
53
}
54
+
55
+
// Release the semaphore permit
42
56
drop(permit);
43
57
});
44
58
59
+
// Store the handle for later cleanup
45
60
tasks.lock().await.push(handle);
46
61
}
47
62
+738
-10
src/actor_store/blob/mod.rs
+738
-10
src/actor_store/blob/mod.rs
···
1
+
// bluepds/src/actor_store/blob/mod.rs
2
+
1
3
//! Blob storage and retrieval for the actor store.
2
4
3
-
mod background;
4
-
mod reader;
5
-
mod store;
6
-
mod transactor;
5
+
use std::str::FromStr;
6
+
7
+
use anyhow::{Context as _, Result, bail};
8
+
use atrium_api::com::atproto::admin::defs::StatusAttr;
9
+
use atrium_repo::Cid;
10
+
use diesel::associations::HasTable as _;
11
+
use diesel::prelude::*;
12
+
use futures::{StreamExt, future::try_join_all};
13
+
use rsky_common::ipld::sha256_raw_to_cid;
14
+
use rsky_pds::actor_store::blob::sha256_stream;
15
+
use rsky_pds::image::{maybe_get_info, mime_type_from_bytes};
16
+
use rsky_pds::schema::pds::*;
17
+
use rsky_repo::types::{PreparedBlobRef, PreparedWrite, WriteOpAction};
18
+
use sha2::Digest;
19
+
use uuid::Uuid;
20
+
21
+
use crate::actor_store::PreparedWrite as BluePreparedWrite;
22
+
use crate::actor_store::db::ActorDb;
23
+
24
+
/// Background task queue for blob operations
25
+
pub mod background;
26
+
// Re-export BackgroundQueue
27
+
pub use background::BackgroundQueue;
28
+
29
+
pub mod placeholder;
30
+
pub(crate) use placeholder::BlobStorePlaceholder;
31
+
32
+
/// Type for stream of blob data
33
+
pub type BlobStream = Box<dyn std::io::Read + Send>;
34
+
35
+
/// Blob store interface
36
+
pub trait BlobStore: Send + Sync {
37
+
async fn put_temp(&self, bytes: &[u8]) -> Result<String>;
38
+
async fn make_permanent(&self, key: &str, cid: Cid) -> Result<()>;
39
+
async fn put_permanent(&self, cid: Cid, bytes: &[u8]) -> Result<()>;
40
+
async fn quarantine(&self, cid: Cid) -> Result<()>;
41
+
async fn unquarantine(&self, cid: Cid) -> Result<()>;
42
+
async fn get_bytes(&self, cid: Cid) -> Result<Vec<u8>>;
43
+
async fn get_stream(&self, cid: Cid) -> Result<BlobStream>;
44
+
async fn has_temp(&self, key: &str) -> Result<bool>;
45
+
async fn has_stored(&self, cid: Cid) -> Result<bool>;
46
+
async fn delete(&self, cid: Cid) -> Result<()>;
47
+
async fn delete_many(&self, cids: Vec<Cid>) -> Result<()>;
48
+
}
49
+
50
+
/// Blob metadata for upload
51
+
pub struct BlobMetadata {
52
+
pub temp_key: String,
53
+
pub size: i64,
54
+
pub cid: Cid,
55
+
pub mime_type: String,
56
+
pub width: Option<i32>,
57
+
pub height: Option<i32>,
58
+
}
59
+
60
+
/// Blob data with content stream
61
+
pub struct BlobData {
62
+
pub size: u64,
63
+
pub mime_type: Option<String>,
64
+
pub stream: BlobStream,
65
+
}
66
+
67
+
/// Options for listing blobs
68
+
pub struct ListBlobsOptions {
69
+
pub since: Option<String>,
70
+
pub cursor: Option<String>,
71
+
pub limit: i64,
72
+
}
73
+
74
+
/// Options for listing missing blobs
75
+
pub struct ListMissingBlobsOptions {
76
+
pub cursor: Option<String>,
77
+
pub limit: i64,
78
+
}
79
+
80
+
/// Information about a missing blob
81
+
pub struct MissingBlob {
82
+
pub cid: String,
83
+
pub record_uri: String,
84
+
}
85
+
86
+
/// Unified handler for blob operations
87
+
pub struct BlobHandler {
88
+
/// Database connection
89
+
pub db: ActorDb,
90
+
/// DID of the actor
91
+
pub did: String,
92
+
/// Blob store implementation
93
+
pub blobstore: Box<dyn BlobStore>,
94
+
/// Background queue for async operations
95
+
pub background_queue: Option<background::BackgroundQueue>,
96
+
}
97
+
98
+
impl BlobHandler {
99
+
/// Create a new blob handler for read operations
100
+
pub fn new_reader(db: ActorDb, blobstore: impl BlobStore + 'static, did: String) -> Self {
101
+
Self {
102
+
db,
103
+
did,
104
+
blobstore: Box::new(blobstore),
105
+
background_queue: None,
106
+
}
107
+
}
108
+
109
+
/// Create a new blob handler with background queue for write operations
110
+
pub fn new_writer(
111
+
db: ActorDb,
112
+
blobstore: impl BlobStore + 'static,
113
+
background_queue: background::BackgroundQueue,
114
+
did: String,
115
+
) -> Self {
116
+
Self {
117
+
db,
118
+
did,
119
+
blobstore: Box::new(blobstore),
120
+
background_queue: Some(background_queue),
121
+
}
122
+
}
123
+
124
+
/// Get metadata for a blob
125
+
pub async fn get_blob_metadata(&self, cid: &Cid) -> Result<BlobMetadata> {
126
+
let cid_str = cid.to_string();
127
+
let did = self.did.clone();
128
+
129
+
let found = self
130
+
.db
131
+
.run(move |conn| {
132
+
blob::table
133
+
.filter(blob::cid.eq(&cid_str))
134
+
.filter(blob::did.eq(&did))
135
+
.filter(blob::takedownRef.is_null())
136
+
.first::<BlobModel>(conn)
137
+
.optional()
138
+
})
139
+
.await?;
140
+
141
+
match found {
142
+
Some(found) => Ok(BlobMetadata {
143
+
temp_key: found.temp_key.unwrap_or_default(),
144
+
size: found.size as i64,
145
+
cid: Cid::from_str(&found.cid)?,
146
+
mime_type: found.mime_type,
147
+
width: found.width,
148
+
height: found.height,
149
+
}),
150
+
None => bail!("Blob not found"),
151
+
}
152
+
}
7
153
8
-
pub(crate) use background::BackgroundQueue;
9
-
pub(crate) use reader::BlobReader;
10
-
pub(crate) use store::BlobStore;
11
-
pub(crate) use store::BlobStorePlaceholder;
12
-
pub(crate) use store::BlobStream;
13
-
pub(crate) use transactor::BlobTransactor;
154
+
/// Get a blob's complete data
155
+
pub async fn get_blob(&self, cid: &Cid) -> Result<BlobData> {
156
+
let metadata = self.get_blob_metadata(cid).await?;
157
+
let blob_stream = self.blobstore.get_stream(*cid).await?;
158
+
159
+
Ok(BlobData {
160
+
size: metadata.size as u64,
161
+
mime_type: Some(metadata.mime_type),
162
+
stream: blob_stream,
163
+
})
164
+
}
165
+
166
+
/// List blobs for a repository
167
+
pub async fn list_blobs(&self, opts: ListBlobsOptions) -> Result<Vec<String>> {
168
+
let did = self.did.clone();
169
+
let since = opts.since;
170
+
let cursor = opts.cursor;
171
+
let limit = opts.limit;
172
+
173
+
self.db
174
+
.run(move |conn| {
175
+
let mut query = record_blob::table
176
+
.inner_join(
177
+
crate::schema::record::table
178
+
.on(crate::schema::record::uri.eq(record_blob::record_uri)),
179
+
)
180
+
.filter(record_blob::did.eq(&did))
181
+
.select(record_blob::blob_cid)
182
+
.distinct()
183
+
.order(record_blob::blob_cid.asc())
184
+
.limit(limit)
185
+
.into_boxed();
186
+
187
+
if let Some(since_val) = since {
188
+
query = query.filter(crate::schema::record::repo_rev.gt(since_val));
189
+
}
190
+
191
+
if let Some(cursor_val) = cursor {
192
+
query = query.filter(record_blob::blob_cid.gt(cursor_val));
193
+
}
194
+
195
+
query.load::<String>(conn)
196
+
})
197
+
.await
198
+
}
199
+
200
+
/// Get records that reference a blob
201
+
pub async fn get_records_for_blob(&self, cid: &Cid) -> Result<Vec<String>> {
202
+
let cid_str = cid.to_string();
203
+
let did = self.did.clone();
204
+
205
+
self.db
206
+
.run(move |conn| {
207
+
record_blob::table
208
+
.filter(record_blob::blob_cid.eq(&cid_str))
209
+
.filter(record_blob::did.eq(&did))
210
+
.select(record_blob::record_uri)
211
+
.load::<String>(conn)
212
+
})
213
+
.await
214
+
}
215
+
216
+
/// Get blobs referenced by a record
217
+
pub async fn get_blobs_for_record(&self, record_uri: &str) -> Result<Vec<String>> {
218
+
let record_uri_str = record_uri.to_string();
219
+
let did = self.did.clone();
220
+
221
+
self.db
222
+
.run(move |conn| {
223
+
blob::table
224
+
.inner_join(record_blob::table.on(record_blob::blob_cid.eq(blob::cid)))
225
+
.filter(record_blob::record_uri.eq(&record_uri_str))
226
+
.filter(blob::did.eq(&did))
227
+
.select(blob::cid)
228
+
.load::<String>(conn)
229
+
})
230
+
.await
231
+
}
232
+
233
+
/// Upload a blob and get its metadata
234
+
pub async fn upload_blob_and_get_metadata(
235
+
&self,
236
+
user_suggested_mime: &str,
237
+
blob_bytes: &[u8],
238
+
) -> Result<BlobMetadata> {
239
+
let temp_key = self.blobstore.put_temp(blob_bytes).await?;
240
+
let size = blob_bytes.len() as i64;
241
+
let sha256 = sha256_stream(blob_bytes).await?;
242
+
let img_info = maybe_get_info(blob_bytes).await?;
243
+
let sniffed_mime = mime_type_from_bytes(blob_bytes).await?;
244
+
let cid = sha256_raw_to_cid(sha256);
245
+
let mime_type = sniffed_mime.unwrap_or_else(|| user_suggested_mime.to_string());
246
+
247
+
Ok(BlobMetadata {
248
+
temp_key,
249
+
size,
250
+
cid,
251
+
mime_type,
252
+
width: img_info.as_ref().map(|info| info.width as i32),
253
+
height: img_info.as_ref().map(|info| info.height as i32),
254
+
})
255
+
}
256
+
257
+
/// Count total blobs
258
+
pub async fn blob_count(&self) -> Result<i64> {
259
+
let did = self.did.clone();
260
+
261
+
self.db
262
+
.run(move |conn| {
263
+
blob::table
264
+
.filter(blob::did.eq(&did))
265
+
.count()
266
+
.get_result(conn)
267
+
})
268
+
.await
269
+
}
270
+
271
+
/// Count distinct blobs referenced by records
272
+
pub async fn record_blob_count(&self) -> Result<i64> {
273
+
let did = self.did.clone();
274
+
275
+
self.db
276
+
.run(move |conn| {
277
+
record_blob::table
278
+
.filter(record_blob::did.eq(&did))
279
+
.select(diesel::dsl::count_distinct(record_blob::blob_cid))
280
+
.first::<i64>(conn)
281
+
})
282
+
.await
283
+
}
284
+
285
+
/// List blobs that are referenced but missing from storage
286
+
pub async fn list_missing_blobs(
287
+
&self,
288
+
opts: ListMissingBlobsOptions,
289
+
) -> Result<Vec<MissingBlob>> {
290
+
let did = self.did.clone();
291
+
let limit = opts.limit;
292
+
let cursor = opts.cursor;
293
+
294
+
self.db
295
+
.run(move |conn| {
296
+
let mut query = record_blob::table
297
+
.left_join(
298
+
blob::table.on(blob::cid.eq(record_blob::blob_cid).and(blob::did.eq(&did))),
299
+
)
300
+
.filter(record_blob::did.eq(&did))
301
+
.filter(blob::cid.is_null())
302
+
.select((record_blob::blob_cid, record_blob::record_uri))
303
+
.order(record_blob::blob_cid.asc())
304
+
.limit(limit)
305
+
.into_boxed();
306
+
307
+
if let Some(cursor_val) = cursor {
308
+
query = query.filter(record_blob::blob_cid.gt(cursor_val));
309
+
}
310
+
311
+
let results = query.load::<(String, String)>(conn)?;
312
+
313
+
Ok(results
314
+
.into_iter()
315
+
.map(|(cid, record_uri)| MissingBlob { cid, record_uri })
316
+
.collect())
317
+
})
318
+
.await
319
+
}
320
+
321
+
/// Get takedown status for a blob
322
+
pub async fn get_blob_takedown_status(&self, cid: &Cid) -> Result<Option<StatusAttr>> {
323
+
let cid_str = cid.to_string();
324
+
let did = self.did.clone();
325
+
326
+
self.db
327
+
.run(move |conn| {
328
+
let result = blob::table
329
+
.filter(blob::cid.eq(&cid_str))
330
+
.filter(blob::did.eq(&did))
331
+
.select(blob::takedownRef)
332
+
.first::<Option<String>>(conn)
333
+
.optional()?;
334
+
335
+
match result {
336
+
Some(takedown) => match takedown {
337
+
Some(takedownRef) => Ok(Some(StatusAttr {
338
+
applied: true,
339
+
r#ref: Some(takedownRef),
340
+
})),
341
+
None => Ok(Some(StatusAttr {
342
+
applied: false,
343
+
r#ref: None,
344
+
})),
345
+
},
346
+
None => Ok(None),
347
+
}
348
+
})
349
+
.await
350
+
}
351
+
352
+
/// Get all blob CIDs in the repository
353
+
pub async fn get_blob_cids(&self) -> Result<Vec<Cid>> {
354
+
let did = self.did.clone();
355
+
356
+
let rows = self
357
+
.db
358
+
.run(move |conn| {
359
+
blob::table
360
+
.filter(blob::did.eq(&did))
361
+
.select(blob::cid)
362
+
.load::<String>(conn)
363
+
})
364
+
.await?;
365
+
366
+
rows.into_iter()
367
+
.map(|cid_str| Cid::from_str(&cid_str).context("Invalid CID format"))
368
+
.collect()
369
+
}
370
+
371
+
/// Track a blob that's not yet associated with a record
372
+
pub async fn track_untethered_blob(&self, metadata: &BlobMetadata) -> Result<()> {
373
+
let cid_str = metadata.cid.to_string();
374
+
let did = self.did.clone();
375
+
376
+
// Check if blob exists and is taken down
377
+
let existing = self
378
+
.db
379
+
.run({
380
+
let cid_str_clone = cid_str.clone();
381
+
let did_clone = did.clone();
382
+
383
+
move |conn| {
384
+
blob::table
385
+
.filter(blob::did.eq(&did_clone))
386
+
.filter(blob::cid.eq(&cid_str_clone))
387
+
.select(blob::takedownRef)
388
+
.first::<Option<String>>(conn)
389
+
.optional()
390
+
}
391
+
})
392
+
.await?;
393
+
394
+
if let Some(row) = existing {
395
+
if row.is_some() {
396
+
return Err(anyhow::anyhow!(
397
+
"Blob has been taken down, cannot re-upload"
398
+
));
399
+
}
400
+
}
401
+
402
+
let size = metadata.size as i32;
403
+
let now = chrono::Utc::now().to_rfc3339();
404
+
let mime_type = metadata.mime_type.clone();
405
+
let temp_key = metadata.temp_key.clone();
406
+
let width = metadata.width;
407
+
let height = metadata.height;
408
+
409
+
self.db.run(move |conn| {
410
+
diesel::insert_into(blob::table)
411
+
.values((
412
+
blob::cid.eq(&cid_str),
413
+
blob::did.eq(&did),
414
+
blob::mime_type.eq(&mime_type),
415
+
blob::size.eq(size),
416
+
blob::temp_key.eq(&temp_key),
417
+
blob::width.eq(width),
418
+
blob::height.eq(height),
419
+
blob::created_at.eq(&now),
420
+
))
421
+
.on_conflict((blob::cid, blob::did))
422
+
.do_update()
423
+
.set(
424
+
blob::temp_key.eq(
425
+
diesel::dsl::sql::<diesel::sql_types::Text>(
426
+
"CASE WHEN blob.temp_key IS NULL THEN excluded.temp_key ELSE blob.temp_key END"
427
+
)
428
+
)
429
+
)
430
+
.execute(conn)
431
+
.context("Failed to track untethered blob")
432
+
}).await?;
433
+
434
+
Ok(())
435
+
}
436
+
437
+
/// Process blobs for repository writes
438
+
pub async fn process_write_blobs(&self, rev: &str, writes: Vec<PreparedWrite>) -> Result<()> {
439
+
self.delete_dereferenced_blobs(writes.clone()).await?;
440
+
441
+
let futures = writes.iter().filter_map(|write| match write {
442
+
PreparedWrite::Create(w) | PreparedWrite::Update(w) => {
443
+
let blobs = &w.blobs;
444
+
let uri = w.uri.clone();
445
+
let handler = self;
446
+
447
+
Some(async move {
448
+
for blob in blobs {
449
+
handler.verify_blob_and_make_permanent(blob).await?;
450
+
handler.associate_blob(blob, &uri).await?;
451
+
}
452
+
Ok(())
453
+
})
454
+
}
455
+
_ => None,
456
+
});
457
+
458
+
try_join_all(futures).await?;
459
+
460
+
Ok(())
461
+
}
462
+
463
+
/// Delete blobs that are no longer referenced
464
+
pub async fn delete_dereferenced_blobs(&self, writes: Vec<PreparedWrite>) -> Result<()> {
465
+
let uris: Vec<String> = writes
466
+
.iter()
467
+
.filter_map(|w| match w {
468
+
PreparedWrite::Delete(w) => Some(w.uri.clone()),
469
+
PreparedWrite::Update(w) => Some(w.uri.clone()),
470
+
_ => None,
471
+
})
472
+
.collect();
473
+
474
+
if uris.is_empty() {
475
+
return Ok(());
476
+
}
477
+
478
+
let did = self.did.clone();
479
+
480
+
// Delete record-blob associations
481
+
let deleted_repo_blobs = self
482
+
.db
483
+
.run({
484
+
let uris_clone = uris.clone();
485
+
let did_clone = did.clone();
486
+
487
+
move |conn| {
488
+
let query = diesel::delete(record_blob::table)
489
+
.filter(record_blob::did.eq(&did_clone))
490
+
.filter(record_blob::record_uri.eq_any(&uris_clone))
491
+
.returning(RecordBlob::as_returning());
492
+
493
+
query.load(conn)
494
+
}
495
+
})
496
+
.await?;
497
+
498
+
if deleted_repo_blobs.is_empty() {
499
+
return Ok(());
500
+
}
501
+
502
+
// Collect deleted blob CIDs
503
+
let deleted_repo_blob_cids: Vec<String> = deleted_repo_blobs
504
+
.iter()
505
+
.map(|rb| rb.blob_cid.clone())
506
+
.collect();
507
+
508
+
// Find duplicates in record_blob table
509
+
let duplicate_cids = self
510
+
.db
511
+
.run({
512
+
let blob_cids = deleted_repo_blob_cids.clone();
513
+
let did_clone = did.clone();
514
+
515
+
move |conn| {
516
+
record_blob::table
517
+
.filter(record_blob::did.eq(&did_clone))
518
+
.filter(record_blob::blob_cid.eq_any(&blob_cids))
519
+
.select(record_blob::blob_cid)
520
+
.load::<String>(conn)
521
+
}
522
+
})
523
+
.await?;
524
+
525
+
// Get new blob CIDs from writes
526
+
let new_blob_cids: Vec<String> = writes
527
+
.iter()
528
+
.filter_map(|w| match w {
529
+
PreparedWrite::Create(w) | PreparedWrite::Update(w) => Some(
530
+
w.blobs
531
+
.iter()
532
+
.map(|b| b.cid.to_string())
533
+
.collect::<Vec<String>>(),
534
+
),
535
+
_ => None,
536
+
})
537
+
.flatten()
538
+
.collect();
539
+
540
+
// Determine which CIDs to keep and which to delete
541
+
let cids_to_keep: std::collections::HashSet<String> = new_blob_cids
542
+
.into_iter()
543
+
.chain(duplicate_cids.into_iter())
544
+
.collect();
545
+
546
+
let cids_to_delete: Vec<String> = deleted_repo_blob_cids
547
+
.into_iter()
548
+
.filter(|cid| !cids_to_keep.contains(cid))
549
+
.collect();
550
+
551
+
if cids_to_delete.is_empty() {
552
+
return Ok(());
553
+
}
554
+
555
+
// Delete blobs from the database
556
+
self.db
557
+
.run({
558
+
let cids = cids_to_delete.clone();
559
+
let did_clone = did.clone();
560
+
561
+
move |conn| {
562
+
diesel::delete(blob::table)
563
+
.filter(blob::did.eq(&did_clone))
564
+
.filter(blob::cid.eq_any(&cids))
565
+
.execute(conn)
566
+
}
567
+
})
568
+
.await?;
569
+
570
+
// Delete blobs from storage
571
+
let cids_to_delete_objects: Vec<Cid> = cids_to_delete
572
+
.iter()
573
+
.filter_map(|cid_str| Cid::from_str(cid_str).ok())
574
+
.collect();
575
+
576
+
// Use background queue if available
577
+
if let Some(queue) = &self.background_queue {
578
+
let blobstore = self.blobstore.clone();
579
+
queue
580
+
.add(async move {
581
+
let _ = blobstore.delete_many(cids_to_delete_objects).await;
582
+
})
583
+
.await;
584
+
} else {
585
+
// Otherwise delete directly
586
+
if !cids_to_delete_objects.is_empty() {
587
+
self.blobstore.delete_many(cids_to_delete_objects).await?;
588
+
}
589
+
}
590
+
591
+
Ok(())
592
+
}
593
+
594
+
/// Verify blob integrity and move from temporary to permanent storage
595
+
pub async fn verify_blob_and_make_permanent(&self, blob: &PreparedBlobRef) -> Result<()> {
596
+
let cid_str = blob.cid.to_string();
597
+
let did = self.did.clone();
598
+
599
+
let found = self
600
+
.db
601
+
.run(move |conn| {
602
+
blob::table
603
+
.filter(blob::did.eq(&did))
604
+
.filter(blob::cid.eq(&cid_str))
605
+
.filter(blob::takedownRef.is_null())
606
+
.first::<BlobModel>(conn)
607
+
.optional()
608
+
})
609
+
.await?;
610
+
611
+
let found = match found {
612
+
Some(b) => b,
613
+
None => bail!("Blob not found: {}", cid_str),
614
+
};
615
+
616
+
// Verify blob constraints
617
+
if let Some(max_size) = blob.constraints.max_size {
618
+
if found.size as usize > max_size {
619
+
bail!(
620
+
"BlobTooLarge: This file is too large. It is {} but the maximum size is {}",
621
+
found.size,
622
+
max_size
623
+
);
624
+
}
625
+
}
626
+
627
+
if blob.mime_type != found.mime_type {
628
+
bail!(
629
+
"InvalidMimeType: Referenced MIME type does not match stored blob. Expected: {}, Got: {}",
630
+
found.mime_type,
631
+
blob.mime_type
632
+
);
633
+
}
634
+
635
+
if let Some(ref accept) = blob.constraints.accept {
636
+
if !accepted_mime(&blob.mime_type, accept).await {
637
+
bail!(
638
+
"Wrong type of file. It is {} but it must match {:?}.",
639
+
blob.mime_type,
640
+
accept
641
+
);
642
+
}
643
+
}
644
+
645
+
// Move blob from temporary to permanent storage if needed
646
+
if let Some(temp_key) = found.temp_key {
647
+
self.blobstore.make_permanent(&temp_key, blob.cid).await?;
648
+
649
+
// Update database to clear temp key
650
+
let cid_str = blob.cid.to_string();
651
+
let did = self.did.clone();
652
+
653
+
self.db
654
+
.run(move |conn| {
655
+
diesel::update(blob::table)
656
+
.filter(blob::did.eq(&did))
657
+
.filter(blob::cid.eq(&cid_str))
658
+
.set(blob::temp_key.eq::<Option<String>>(None))
659
+
.execute(conn)
660
+
})
661
+
.await?;
662
+
}
663
+
664
+
Ok(())
665
+
}
666
+
667
+
/// Associate a blob with a record
668
+
pub async fn associate_blob(&self, blob: &PreparedBlobRef, record_uri: &str) -> Result<()> {
669
+
let cid_str = blob.cid.to_string();
670
+
let record_uri = record_uri.to_string();
671
+
let did = self.did.clone();
672
+
673
+
self.db
674
+
.run(move |conn| {
675
+
diesel::insert_into(record_blob::table)
676
+
.values((
677
+
record_blob::blob_cid.eq(&cid_str),
678
+
record_blob::record_uri.eq(&record_uri),
679
+
record_blob::did.eq(&did),
680
+
))
681
+
.on_conflict_do_nothing()
682
+
.execute(conn)
683
+
})
684
+
.await?;
685
+
686
+
Ok(())
687
+
}
688
+
689
+
/// Update takedown status for a blob
690
+
pub async fn update_blob_takedown_status(&self, blob: Cid, takedown: StatusAttr) -> Result<()> {
691
+
let cid_str = blob.to_string();
692
+
let did = self.did.clone();
693
+
694
+
let takedownRef: Option<String> = if takedown.applied {
695
+
Some(takedown.r#ref.unwrap_or_else(|| Uuid::new_v4().to_string()))
696
+
} else {
697
+
None
698
+
};
699
+
700
+
// Update database
701
+
self.db
702
+
.run(move |conn| {
703
+
diesel::update(blob::table)
704
+
.filter(blob::did.eq(&did))
705
+
.filter(blob::cid.eq(&cid_str))
706
+
.set(blob::takedownRef.eq(takedownRef))
707
+
.execute(conn)
708
+
})
709
+
.await?;
710
+
711
+
// Update blob storage
712
+
if takedown.applied {
713
+
self.blobstore.quarantine(blob).await?;
714
+
} else {
715
+
self.blobstore.unquarantine(blob).await?;
716
+
}
717
+
718
+
Ok(())
719
+
}
720
+
}
721
+
722
+
/// Verify MIME type against accepted formats
723
+
async fn accepted_mime(mime: &str, accepted: &[String]) -> bool {
724
+
// Accept any type
725
+
if accepted.contains(&"*/*".to_string()) {
726
+
return true;
727
+
}
728
+
729
+
// Check for glob patterns (e.g., "image/*")
730
+
for glob in accepted {
731
+
if glob.ends_with("/*") {
732
+
let prefix = glob.split('/').next().unwrap();
733
+
if mime.starts_with(&format!("{}/", prefix)) {
734
+
return true;
735
+
}
736
+
}
737
+
}
738
+
739
+
// Check for exact match
740
+
accepted.contains(&mime.to_string())
741
+
}
+54
src/actor_store/blob/placeholder.rs
+54
src/actor_store/blob/placeholder.rs
···
1
+
use anyhow::Result;
2
+
use atrium_repo::Cid;
3
+
4
+
use super::{BlobStore, BlobStream};
5
+
6
+
/// Placeholder implementation for blob store
7
+
#[derive(Clone)]
8
+
pub struct BlobStorePlaceholder;
9
+
10
+
impl BlobStore for BlobStorePlaceholder {
11
+
async fn put_temp(&self, _bytes: &[u8]) -> Result<String> {
12
+
todo!("BlobStorePlaceholder::put_temp not implemented");
13
+
}
14
+
15
+
async fn make_permanent(&self, _key: &str, _cid: Cid) -> Result<()> {
16
+
todo!("BlobStorePlaceholder::make_permanent not implemented");
17
+
}
18
+
19
+
async fn put_permanent(&self, _cid: Cid, _bytes: &[u8]) -> Result<()> {
20
+
todo!("BlobStorePlaceholder::put_permanent not implemented");
21
+
}
22
+
23
+
async fn quarantine(&self, _cid: Cid) -> Result<()> {
24
+
todo!("BlobStorePlaceholder::quarantine not implemented");
25
+
}
26
+
27
+
async fn unquarantine(&self, _cid: Cid) -> Result<()> {
28
+
todo!("BlobStorePlaceholder::unquarantine not implemented");
29
+
}
30
+
31
+
async fn get_bytes(&self, _cid: Cid) -> Result<Vec<u8>> {
32
+
todo!("BlobStorePlaceholder::get_bytes not implemented");
33
+
}
34
+
35
+
async fn get_stream(&self, _cid: Cid) -> Result<BlobStream> {
36
+
todo!("BlobStorePlaceholder::get_stream not implemented");
37
+
}
38
+
39
+
async fn has_temp(&self, _key: &str) -> Result<bool> {
40
+
todo!("BlobStorePlaceholder::has_temp not implemented");
41
+
}
42
+
43
+
async fn has_stored(&self, _cid: Cid) -> Result<bool> {
44
+
todo!("BlobStorePlaceholder::has_stored not implemented");
45
+
}
46
+
47
+
async fn delete(&self, _cid: Cid) -> Result<()> {
48
+
todo!("BlobStorePlaceholder::delete not implemented");
49
+
}
50
+
51
+
async fn delete_many(&self, _cids: Vec<Cid>) -> Result<()> {
52
+
todo!("BlobStorePlaceholder::delete_many not implemented");
53
+
}
54
+
}
-261
src/actor_store/blob/reader.rs
-261
src/actor_store/blob/reader.rs
···
1
-
//! Blob reading functionality.
2
-
3
-
use std::str::FromStr;
4
-
5
-
use anyhow::{Context as _, Result};
6
-
use atrium_api::com::atproto::admin::defs::StatusAttrData;
7
-
use atrium_repo::Cid;
8
-
use sqlx::Row;
9
-
10
-
use crate::actor_store::ActorDb;
11
-
12
-
use super::{BlobStore, BlobStorePlaceholder, BlobStream};
13
-
14
-
/// Reader for blob data in the actor store.
15
-
pub(crate) struct BlobReader {
16
-
/// Database connection.
17
-
pub db: ActorDb,
18
-
/// BlobStore.
19
-
pub blobstore: BlobStorePlaceholder,
20
-
}
21
-
22
-
impl BlobReader {
23
-
/// Create a new blob reader.
24
-
pub(crate) fn new(db: ActorDb, blobstore: BlobStorePlaceholder) -> Self {
25
-
Self { db, blobstore }
26
-
}
27
-
28
-
/// Get metadata for a blob.
29
-
pub(crate) async fn get_blob_metadata(&self, cid: &Cid) -> Result<Option<BlobMetadata>> {
30
-
let cid_str = cid.to_string();
31
-
let found = sqlx::query!(
32
-
r#"
33
-
SELECT mimeType, size, takedownRef
34
-
FROM blob
35
-
WHERE cid = ? AND takedownRef IS NULL
36
-
"#,
37
-
cid_str
38
-
)
39
-
.fetch_optional(&self.db.pool)
40
-
.await
41
-
.context("failed to fetch blob metadata")?;
42
-
if found.is_none() {
43
-
return Err(anyhow::anyhow!("Blob not found")); // InvalidRequestError('Blob not found')
44
-
}
45
-
let found = found.unwrap();
46
-
let size = found.size as u64;
47
-
let mime_type = found.mimeType;
48
-
return Ok(Some(BlobMetadata { size, mime_type }));
49
-
}
50
-
51
-
/// Get a blob's full data and metadata.
52
-
pub(crate) async fn get_blob(&self, cid: &Cid) -> Result<Option<BlobData>> {
53
-
let metadata = self.get_blob_metadata(cid).await?;
54
-
let blob_stream = self.blobstore.get_stream(*cid).await;
55
-
if blob_stream.is_err() {
56
-
return Err(anyhow::anyhow!("Blob not found")); // InvalidRequestError('Blob not found')
57
-
}
58
-
let metadata = metadata.unwrap();
59
-
Ok(Some(BlobData {
60
-
size: metadata.size,
61
-
mime_type: Some(metadata.mime_type),
62
-
stream: blob_stream.unwrap(),
63
-
}))
64
-
}
65
-
66
-
/// List blobs for a repository.
67
-
pub(crate) async fn list_blobs(&self, opts: ListBlobsOptions) -> Result<Vec<String>> {
68
-
let mut query = sqlx::QueryBuilder::new(
69
-
"SELECT rb.blobCid FROM record_blob rb
70
-
INNER JOIN record r ON r.uri = rb.recordUri",
71
-
);
72
-
if let Some(since) = &opts.since {
73
-
query.push(" WHERE r.repoRev > ").push_bind(since);
74
-
}
75
-
if let Some(cursor) = &opts.cursor {
76
-
query.push(" AND rb.blobCid > ").push_bind(cursor);
77
-
}
78
-
query
79
-
.push(" ORDER BY rb.blobCid ASC")
80
-
.push(" LIMIT ")
81
-
.push_bind(opts.limit);
82
-
let blobs = query
83
-
.build()
84
-
.map(|row: sqlx::sqlite::SqliteRow| row.get::<String, _>(0))
85
-
.fetch_all(&self.db.pool)
86
-
.await
87
-
.context("failed to fetch blobs")?;
88
-
Ok(blobs)
89
-
}
90
-
91
-
/// Get takedown status for a blob.
92
-
pub(crate) async fn get_blob_takedown_status(
93
-
&self,
94
-
cid: &Cid,
95
-
) -> Result<Option<StatusAttrData>> {
96
-
let cid_str = cid.to_string();
97
-
let result = sqlx::query!(r#"SELECT takedownRef FROM blob WHERE cid = ?"#, cid_str)
98
-
.fetch_optional(&self.db.pool)
99
-
.await
100
-
.context("failed to fetch blob takedown status")?;
101
-
102
-
Ok({
103
-
if result.is_none() {
104
-
None
105
-
} else {
106
-
let takedown_ref = result.unwrap().takedownRef.unwrap();
107
-
if takedown_ref == "false" {
108
-
Some(StatusAttrData {
109
-
applied: false,
110
-
r#ref: None,
111
-
})
112
-
} else {
113
-
Some(StatusAttrData {
114
-
applied: true,
115
-
r#ref: Some(takedown_ref),
116
-
})
117
-
}
118
-
}
119
-
})
120
-
}
121
-
122
-
/// Get records that reference a blob.
123
-
pub(crate) async fn get_records_for_blob(&self, cid: &Cid) -> Result<Vec<String>> {
124
-
let cid_str = cid.to_string();
125
-
let records = sqlx::query!(
126
-
r#"SELECT recordUri FROM record_blob WHERE blobCid = ?"#,
127
-
cid_str
128
-
)
129
-
.fetch_all(&self.db.pool)
130
-
.await
131
-
.context("failed to fetch records for blob")?;
132
-
133
-
Ok(records.into_iter().map(|r| r.recordUri).collect())
134
-
}
135
-
136
-
/// Get blobs referenced by a record.
137
-
pub(crate) async fn get_blobs_for_record(&self, record_uri: &str) -> Result<Vec<String>> {
138
-
let blobs = sqlx::query!(
139
-
r#"SELECT blob.cid FROM blob INNER JOIN record_blob ON record_blob.blobCid = blob.cid WHERE recordUri = ?"#,
140
-
record_uri
141
-
)
142
-
.fetch_all(&self.db.pool)
143
-
.await
144
-
.context("failed to fetch blobs for record")?;
145
-
146
-
Ok(blobs.into_iter().map(|blob| blob.cid).collect())
147
-
}
148
-
149
-
/// Count total blobs.
150
-
pub(crate) async fn blob_count(&self) -> Result<i64> {
151
-
let result = sqlx::query!(r#"SELECT COUNT(*) as count FROM blob"#)
152
-
.fetch_one(&self.db.pool)
153
-
.await
154
-
.context("failed to count blobs")?;
155
-
156
-
Ok(result.count)
157
-
}
158
-
159
-
/// Count distinct blobs referenced by records.
160
-
pub(crate) async fn record_blob_count(&self) -> Result<i64> {
161
-
let result = sqlx::query!(r#"SELECT COUNT(DISTINCT blobCid) as count FROM record_blob"#)
162
-
.fetch_one(&self.db.pool)
163
-
.await
164
-
.context("failed to count record blobs")?;
165
-
166
-
Ok(result.count)
167
-
}
168
-
169
-
/// List blobs that are referenced but missing from storage.
170
-
pub(crate) async fn list_missing_blobs(
171
-
&self,
172
-
opts: ListMissingBlobsOptions,
173
-
) -> Result<Vec<MissingBlob>> {
174
-
let mut query = sqlx::QueryBuilder::new(
175
-
"SELECT rb.blobCid, rb.recordUri FROM record_blob rb
176
-
WHERE NOT EXISTS (
177
-
SELECT 1 FROM blob b WHERE b.cid = rb.blobCid
178
-
)",
179
-
);
180
-
181
-
if let Some(cursor) = &opts.cursor {
182
-
query.push(" AND rb.blobCid > ").push_bind(cursor);
183
-
}
184
-
185
-
query
186
-
.push(" ORDER BY rb.blobCid ASC")
187
-
.push(" LIMIT ")
188
-
.push_bind(opts.limit);
189
-
190
-
let missing = query
191
-
.build()
192
-
.map(|row: sqlx::sqlite::SqliteRow| MissingBlob {
193
-
cid: row.get::<String, _>(0),
194
-
record_uri: row.get::<String, _>(1),
195
-
})
196
-
.fetch_all(&self.db.pool)
197
-
.await
198
-
.context("failed to fetch missing blobs")?;
199
-
200
-
Ok(missing)
201
-
}
202
-
203
-
pub(crate) async fn get_blod_cids(&self) -> Result<Vec<Cid>> {
204
-
let rows = sqlx::query!("SELECT cid FROM blob")
205
-
.fetch_all(&self.db.pool)
206
-
.await
207
-
.context("failed to fetch blob CIDs")?;
208
-
Ok(rows
209
-
.into_iter()
210
-
.map(|row| Cid::from_str(&row.cid).unwrap())
211
-
.collect())
212
-
}
213
-
}
214
-
215
-
/// Metadata about a blob.
216
-
#[derive(Debug, Clone)]
217
-
pub(crate) struct BlobMetadata {
218
-
/// The size of the blob in bytes.
219
-
pub size: u64,
220
-
/// The MIME type of the blob.
221
-
pub mime_type: String,
222
-
}
223
-
224
-
/// Complete blob data with content.
225
-
#[derive(Debug)]
226
-
pub(crate) struct BlobData {
227
-
/// The size of the blob.
228
-
pub size: u64,
229
-
/// The MIME type of the blob.
230
-
pub mime_type: Option<String>,
231
-
pub stream: BlobStream, // stream.Readable,
232
-
}
233
-
234
-
/// Options for listing blobs.
235
-
#[derive(Debug, Clone)]
236
-
pub(crate) struct ListBlobsOptions {
237
-
/// Optional revision to list blobs since.
238
-
pub since: Option<String>,
239
-
/// Optional cursor for pagination.
240
-
pub cursor: Option<String>,
241
-
/// Maximum number of blobs to return.
242
-
pub limit: i64,
243
-
}
244
-
245
-
/// Options for listing missing blobs.
246
-
#[derive(Debug, Clone)]
247
-
pub(crate) struct ListMissingBlobsOptions {
248
-
/// Optional cursor for pagination.
249
-
pub cursor: Option<String>,
250
-
/// Maximum number of missing blobs to return.
251
-
pub limit: i64,
252
-
}
253
-
254
-
/// Information about a missing blob.
255
-
#[derive(Debug, Clone)]
256
-
pub(crate) struct MissingBlob {
257
-
/// CID of the missing blob.
258
-
pub cid: String,
259
-
/// URI of the record referencing the missing blob.
260
-
pub record_uri: String,
261
-
}
-92
src/actor_store/blob/store.rs
-92
src/actor_store/blob/store.rs
···
1
-
use anyhow::Result;
2
-
use atrium_repo::Cid;
3
-
use std::fmt::Debug;
4
-
5
-
/// BlobStream
6
-
/// A stream of blob data.
7
-
pub(crate) struct BlobStream(Box<dyn std::io::Read + Send>);
8
-
impl Debug for BlobStream {
9
-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10
-
f.debug_struct("BlobStream").finish()
11
-
}
12
-
}
13
-
14
-
pub(crate) struct BlobStorePlaceholder;
15
-
pub(crate) trait BlobStore {
16
-
async fn put_temp(&self, bytes: &[u8]) -> Result<String>; // bytes: Uint8Array | stream.Readable
17
-
async fn make_permanent(&self, key: &str, cid: Cid) -> Result<()>;
18
-
async fn put_permanent(&self, cid: Cid, bytes: &[u8]) -> Result<()>;
19
-
async fn quarantine(&self, cid: Cid) -> Result<()>;
20
-
async fn unquarantine(&self, cid: Cid) -> Result<()>;
21
-
async fn get_bytes(&self, cid: Cid) -> Result<Vec<u8>>;
22
-
async fn get_stream(&self, cid: Cid) -> Result<BlobStream>; // Promise<stream.Readable>
23
-
async fn has_temp(&self, key: &str) -> Result<bool>;
24
-
async fn has_stored(&self, cid: Cid) -> Result<bool>;
25
-
async fn delete(&self, cid: Cid) -> Result<()>;
26
-
async fn delete_many(&self, cids: Vec<Cid>) -> Result<()>;
27
-
}
28
-
impl BlobStore for BlobStorePlaceholder {
29
-
async fn put_temp(&self, bytes: &[u8]) -> Result<String> {
30
-
// Implementation here
31
-
todo!();
32
-
Ok("temp_key".to_string())
33
-
}
34
-
35
-
async fn make_permanent(&self, key: &str, cid: Cid) -> Result<()> {
36
-
// Implementation here
37
-
todo!();
38
-
Ok(())
39
-
}
40
-
41
-
async fn put_permanent(&self, cid: Cid, bytes: &[u8]) -> Result<()> {
42
-
// Implementation here
43
-
todo!();
44
-
Ok(())
45
-
}
46
-
47
-
async fn quarantine(&self, cid: Cid) -> Result<()> {
48
-
// Implementation here
49
-
todo!();
50
-
Ok(())
51
-
}
52
-
53
-
async fn unquarantine(&self, cid: Cid) -> Result<()> {
54
-
// Implementation here
55
-
todo!();
56
-
Ok(())
57
-
}
58
-
59
-
async fn get_bytes(&self, cid: Cid) -> Result<Vec<u8>> {
60
-
// Implementation here
61
-
todo!();
62
-
Ok(vec![])
63
-
}
64
-
65
-
async fn get_stream(&self, cid: Cid) -> Result<BlobStream> {
66
-
// Implementation here
67
-
todo!();
68
-
Ok(BlobStream(Box::new(std::io::empty())))
69
-
}
70
-
71
-
async fn has_temp(&self, key: &str) -> Result<bool> {
72
-
// Implementation here
73
-
todo!();
74
-
Ok(true)
75
-
}
76
-
77
-
async fn has_stored(&self, cid: Cid) -> Result<bool> {
78
-
// Implementation here
79
-
todo!();
80
-
Ok(true)
81
-
}
82
-
async fn delete(&self, cid: Cid) -> Result<()> {
83
-
// Implementation here
84
-
todo!();
85
-
Ok(())
86
-
}
87
-
async fn delete_many(&self, cids: Vec<Cid>) -> Result<()> {
88
-
// Implementation here
89
-
todo!();
90
-
Ok(())
91
-
}
92
-
}
-399
src/actor_store/blob/transactor.rs
-399
src/actor_store/blob/transactor.rs
···
1
-
//! Blob transaction functionality.
2
-
3
-
use anyhow::{Context as _, Result};
4
-
use atrium_api::{
5
-
com::atproto::admin::defs::StatusAttr,
6
-
types::{Blob, CidLink},
7
-
};
8
-
use atrium_repo::Cid;
9
-
use futures::FutureExt;
10
-
use futures::future::BoxFuture;
11
-
use rsky_repo::types::{PreparedBlobRef, WriteOpAction};
12
-
use uuid::Uuid;
13
-
14
-
use super::{BackgroundQueue, BlobReader, BlobStore};
15
-
use crate::actor_store::{ActorDb, PreparedWrite, blob::BlobStore as _};
16
-
17
-
/// Blob metadata for a newly uploaded blob.
18
-
#[derive(Debug, Clone)]
19
-
pub(crate) struct BlobMetadata {
20
-
/// Temporary key for the blob during upload.
21
-
pub temp_key: String,
22
-
/// Size of the blob in bytes.
23
-
pub size: u64,
24
-
/// CID of the blob.
25
-
pub cid: Cid,
26
-
/// MIME type of the blob.
27
-
pub mime_type: String,
28
-
/// Optional width if the blob is an image.
29
-
pub width: Option<i32>,
30
-
/// Optional height if the blob is an image.
31
-
pub height: Option<i32>,
32
-
}
33
-
34
-
/// Transactor for blob operations.
35
-
pub(crate) struct BlobTransactor {
36
-
/// The blob reader.
37
-
pub reader: BlobReader,
38
-
pub background_queue: BackgroundQueue,
39
-
}
40
-
41
-
impl BlobTransactor {
42
-
/// Create a new blob transactor.
43
-
pub(crate) fn new(
44
-
db: ActorDb,
45
-
blob_store: BlobStore,
46
-
background_queue: BackgroundQueue,
47
-
) -> Self {
48
-
Self {
49
-
reader: BlobReader::new(db, blob_store),
50
-
background_queue,
51
-
}
52
-
}
53
-
54
-
/// Register blob associations with records.
55
-
pub(crate) async fn insert_blobs(&self, record_uri: &str, blobs: &[Blob]) -> Result<()> {
56
-
if blobs.is_empty() {
57
-
return Ok(());
58
-
}
59
-
60
-
let mut query =
61
-
sqlx::QueryBuilder::new("INSERT INTO record_blob (recordUri, blobCid) VALUES ");
62
-
63
-
for (i, blob) in blobs.iter().enumerate() {
64
-
if i > 0 {
65
-
query.push(", ");
66
-
}
67
-
68
-
let cid_str = blob.r#ref.0.to_string();
69
-
query
70
-
.push("(")
71
-
.push_bind(record_uri)
72
-
.push(", ")
73
-
.push_bind(cid_str)
74
-
.push(")");
75
-
}
76
-
77
-
query.push(" ON CONFLICT DO NOTHING");
78
-
79
-
query
80
-
.build()
81
-
.execute(&self.reader.db.pool)
82
-
.await
83
-
.context("failed to insert blob references")?;
84
-
85
-
Ok(())
86
-
}
87
-
88
-
/// Upload a blob and get its metadata.
89
-
pub(crate) async fn upload_blob_and_get_metadata(
90
-
&self,
91
-
user_suggested_mime: &str,
92
-
blob_stream: &[u8],
93
-
) -> Result<BlobMetadata> {
94
-
let temp_key = self.reader.blobstore.put_temp(blob_stream).await?;
95
-
todo!();
96
-
let size = stream_size(blob_stream).await?;
97
-
let sha256 = sha256_stream(blob_stream).await?;
98
-
let img_info = img::maybe_get_info(blob_stream).await?;
99
-
let sniffed_mime = mime_type_from_stream(blob_stream).await?;
100
-
let cid = sha256_raw_to_cid(sha256);
101
-
let mime_type = sniffed_mime.unwrap_or_else(|| user_suggested_mime.to_string());
102
-
Ok(BlobMetadata {
103
-
temp_key,
104
-
size,
105
-
cid,
106
-
mime_type,
107
-
width: img_info.map(|info| info.width),
108
-
height: img_info.map(|info| info.height),
109
-
})
110
-
}
111
-
112
-
/// Track a new blob that's not yet associated with a record.
113
-
pub(crate) async fn track_untethered_blob(&self, metadata: &BlobMetadata) -> Result<Blob> {
114
-
let cid_str = metadata.cid.to_string();
115
-
116
-
// Check if blob exists and is taken down
117
-
let existing = sqlx::query!(r#"SELECT takedownRef FROM blob WHERE cid = ?"#, cid_str)
118
-
.fetch_optional(&self.reader.db.pool)
119
-
.await
120
-
.context("failed to check blob existence")?;
121
-
122
-
if let Some(row) = existing {
123
-
if row.takedownRef.is_some() {
124
-
return Err(anyhow::anyhow!(
125
-
"Blob has been taken down, cannot re-upload"
126
-
));
127
-
}
128
-
}
129
-
130
-
// Insert or update blob record
131
-
let size = metadata.size as i64;
132
-
let now = chrono::Utc::now().to_rfc3339();
133
-
sqlx::query!(
134
-
r#"
135
-
INSERT INTO blob
136
-
(cid, mimeType, size, tempKey, width, height, createdAt)
137
-
VALUES (?, ?, ?, ?, ?, ?, ?)
138
-
ON CONFLICT(cid) DO UPDATE SET
139
-
tempKey = CASE
140
-
WHEN blob.tempKey IS NULL THEN EXCLUDED.tempKey
141
-
ELSE blob.tempKey
142
-
END
143
-
"#,
144
-
cid_str,
145
-
metadata.mime_type,
146
-
size,
147
-
metadata.temp_key,
148
-
metadata.width,
149
-
metadata.height,
150
-
now,
151
-
)
152
-
.execute(&self.reader.db.pool)
153
-
.await
154
-
.context("failed to track blob in database")?;
155
-
156
-
// Create and return a blob reference
157
-
Ok(Blob {
158
-
r#ref: CidLink(metadata.cid.clone()),
159
-
mime_type: metadata.mime_type.clone(),
160
-
size: metadata.size as usize,
161
-
})
162
-
}
163
-
164
-
/// Process blobs for a repository write operation.
165
-
pub(crate) async fn process_write_blobs(
166
-
&self,
167
-
_rev: &String, // Typescript impl declares rev but never uses it
168
-
writes: Vec<PreparedWrite>,
169
-
) -> Result<()> {
170
-
self.delete_dereferenced_blobs(writes.clone(), false)
171
-
.await
172
-
.context("failed to delete dereferenced blobs")?;
173
-
174
-
// Process blobs for creates and updates
175
-
let mut futures: Vec<BoxFuture<'_, Result<()>>> = Vec::new();
176
-
for write in writes.iter() {
177
-
if write.action() == &WriteOpAction::Create || write.action() == &WriteOpAction::Update
178
-
{
179
-
for blob in write.blobs().unwrap().iter() {
180
-
futures.push(Box::pin(self.verify_blob_and_make_permanent(blob)));
181
-
futures.push(Box::pin(self.associate_blob(blob, write.uri())));
182
-
}
183
-
}
184
-
}
185
-
186
-
// Wait for all blob operations to complete
187
-
futures::future::join_all(futures)
188
-
.await
189
-
.into_iter()
190
-
.collect::<Result<Vec<_>>>()?;
191
-
192
-
Ok(())
193
-
}
194
-
195
-
/// Update the takedown status of a blob.
196
-
pub(crate) async fn update_blob_takedown_status(
197
-
&self,
198
-
blob: &Blob,
199
-
takedown: &StatusAttr,
200
-
) -> Result<()> {
201
-
let takedown_ref = if takedown.applied {
202
-
Some(
203
-
takedown
204
-
.r#ref
205
-
.clone()
206
-
.unwrap_or_else(|| Uuid::new_v4().to_string()),
207
-
)
208
-
} else {
209
-
None
210
-
};
211
-
212
-
let cid_str = blob.r#ref.0.to_string();
213
-
sqlx::query!(
214
-
r#"UPDATE blob SET takedownRef = ? WHERE cid = ?"#,
215
-
takedown_ref,
216
-
cid_str
217
-
)
218
-
.execute(&self.reader.db.pool)
219
-
.await
220
-
.context("failed to update blob takedown status")?;
221
-
222
-
Ok(())
223
-
}
224
-
225
-
/// Delete blobs that are no longer referenced by any record.
226
-
pub(crate) async fn delete_dereferenced_blobs(
227
-
&self,
228
-
writes: Vec<PreparedWrite>,
229
-
skip_blob_store: bool,
230
-
) -> Result<()> {
231
-
let deletes = writes
232
-
.iter()
233
-
.filter(|w| w.action() == &WriteOpAction::Delete)
234
-
.collect::<Vec<_>>();
235
-
let updates = writes
236
-
.iter()
237
-
.filter(|w| w.action() == &WriteOpAction::Update)
238
-
.collect::<Vec<_>>();
239
-
let uris: Vec<String> = deletes
240
-
.iter()
241
-
.chain(updates.iter())
242
-
.map(|w| w.uri().to_string())
243
-
.collect();
244
-
245
-
if uris.is_empty() {
246
-
return Ok(());
247
-
}
248
-
249
-
// Delete blobs from record_blob table
250
-
let uris = uris.join(",");
251
-
let deleted_repo_blobs = sqlx::query!(
252
-
r#"DELETE FROM record_blob WHERE recordUri IN (?1) RETURNING *"#,
253
-
uris
254
-
)
255
-
.fetch_all(&self.reader.db.pool)
256
-
.await
257
-
.context("failed to delete dereferenced blobs")?;
258
-
259
-
if deleted_repo_blobs.is_empty() {
260
-
return Ok(());
261
-
}
262
-
263
-
// Get the CIDs of the deleted blobs
264
-
let deleted_repo_blob_cids: Vec<String> = deleted_repo_blobs
265
-
.iter()
266
-
.map(|row| row.blobCid.clone())
267
-
.collect();
268
-
269
-
// Check for duplicates in the record_blob table
270
-
let duplicate_cids = deleted_repo_blob_cids.join(",");
271
-
let duplicate_cids = sqlx::query!(
272
-
r#"SELECT blobCid FROM record_blob WHERE blobCid IN (?1)"#,
273
-
duplicate_cids
274
-
)
275
-
.fetch_all(&self.reader.db.pool)
276
-
.await
277
-
.context("failed to fetch duplicate CIDs")?;
278
-
279
-
// Get new blob CIDs from the writes
280
-
let new_blob_cids: Vec<String> = writes
281
-
.iter()
282
-
.filter_map(|w| {
283
-
if w.action() == &WriteOpAction::Create || w.action() == &WriteOpAction::Update {
284
-
Some(
285
-
w.blobs()
286
-
.unwrap()
287
-
.iter()
288
-
.map(|b| b.cid.to_string())
289
-
.collect::<Vec<String>>(),
290
-
)
291
-
} else {
292
-
None
293
-
}
294
-
})
295
-
.flatten()
296
-
.collect();
297
-
298
-
// Determine which CIDs to keep and which to delete
299
-
let cids_to_keep: Vec<String> = new_blob_cids
300
-
.into_iter()
301
-
.chain(duplicate_cids.into_iter().map(|row| row.blobCid))
302
-
.collect();
303
-
let cids_to_delete: Vec<String> = deleted_repo_blob_cids
304
-
.into_iter()
305
-
.filter(|cid| !cids_to_keep.contains(cid))
306
-
.collect();
307
-
if cids_to_delete.is_empty() {
308
-
return Ok(());
309
-
}
310
-
// Delete blobs from the blob table
311
-
let cids_to_delete = cids_to_delete.join(",");
312
-
sqlx::query!(r#"DELETE FROM blob WHERE cid IN (?1)"#, cids_to_delete)
313
-
.execute(&self.reader.db.pool)
314
-
.await
315
-
.context("failed to delete dereferenced blobs from blob table")?;
316
-
// Optionally delete blobs from the blob store
317
-
if !skip_blob_store {
318
-
todo!();
319
-
}
320
-
Ok(())
321
-
}
322
-
323
-
/// Verify a blob's integrity and move it from temporary to permanent storage.
324
-
pub(crate) async fn verify_blob_and_make_permanent(
325
-
&self,
326
-
blob: &PreparedBlobRef,
327
-
) -> Result<()> {
328
-
let cid_str = blob.cid.to_string();
329
-
let found = sqlx::query!(r#"SELECT * FROM blob WHERE cid = ?"#, cid_str)
330
-
.fetch_optional(&self.reader.db.pool)
331
-
.await
332
-
.context("failed to fetch blob")?;
333
-
if found.is_none() {
334
-
return Err(anyhow::anyhow!("Blob not found"));
335
-
}
336
-
let found = found.unwrap();
337
-
if found.takedownRef.is_some() {
338
-
return Err(anyhow::anyhow!("Blob has been taken down"));
339
-
}
340
-
if found.tempKey.is_some() {
341
-
todo!("verify_blob");
342
-
verify_blob(blob, found);
343
-
self.reader
344
-
.blobstore
345
-
.make_permanent(&found.tempKey.unwrap(), blob.cid)
346
-
.await?;
347
-
sqlx::query!(
348
-
r#"UPDATE blob SET tempKey = NULL WHERE tempKey = ?"#,
349
-
found.tempKey
350
-
)
351
-
.execute(&self.reader.db.pool)
352
-
.await
353
-
.context("failed to update blob temp key")?;
354
-
}
355
-
Ok(())
356
-
}
357
-
358
-
/// Associate a blob with a record
359
-
pub(crate) async fn associate_blob(
360
-
&self,
361
-
blob: &PreparedBlobRef,
362
-
record_uri: &str,
363
-
) -> Result<()> {
364
-
let cid = blob.cid.to_string();
365
-
sqlx::query!(
366
-
r#"
367
-
INSERT INTO record_blob (blobCid, recordUri)
368
-
VALUES (?, ?)
369
-
ON CONFLICT DO NOTHING
370
-
"#,
371
-
cid,
372
-
record_uri
373
-
)
374
-
.execute(&self.reader.db.pool)
375
-
.await
376
-
.context("failed to associate blob with record")?;
377
-
378
-
Ok(())
379
-
}
380
-
}
381
-
382
-
/// Check if a mime type is accepted based on the accept list.
383
-
fn accepted_mime(mime: &str, accepted: &[String]) -> bool {
384
-
// Accept all types
385
-
if accepted.contains(&"*/*".to_string()) {
386
-
return true;
387
-
}
388
-
389
-
// Check for explicit match
390
-
if accepted.contains(&mime.to_string()) {
391
-
return true;
392
-
}
393
-
394
-
// Check for type/* matches
395
-
let type_prefix = mime.split('/').next().unwrap_or("");
396
-
let wildcard = format!("{}/*", type_prefix);
397
-
398
-
accepted.contains(&wildcard)
399
-
}
+53
src/actor_store/db.rs
+53
src/actor_store/db.rs
···
1
+
//! Database schema and connection management for the actor store.
2
+
3
+
use crate::db::DatabaseConnection;
4
+
use anyhow::{Context as _, Result};
5
+
use diesel::prelude::*;
6
+
7
+
/// Type alias for the actor database.
8
+
pub(crate) type ActorDb = DatabaseConnection;
9
+
10
+
/// Gets a database connection for the actor store.
11
+
///
12
+
/// # Arguments
13
+
///
14
+
/// * `location` - The file path or URI for the SQLite database.
15
+
/// * `disable_wal_auto_checkpoint` - Whether to disable the WAL auto-checkpoint.
16
+
///
17
+
/// # Returns
18
+
///
19
+
/// A `Result` containing the `ActorDb` instance or an error.
20
+
pub async fn get_db(location: &str, disable_wal_auto_checkpoint: bool) -> Result<ActorDb> {
21
+
let pragmas = if disable_wal_auto_checkpoint {
22
+
Some(
23
+
&[
24
+
("wal_autocheckpoint", "0"),
25
+
("journal_mode", "WAL"),
26
+
("synchronous", "NORMAL"),
27
+
("foreign_keys", "ON"),
28
+
][..],
29
+
)
30
+
} else {
31
+
Some(
32
+
&[
33
+
("journal_mode", "WAL"),
34
+
("synchronous", "NORMAL"),
35
+
("foreign_keys", "ON"),
36
+
][..],
37
+
)
38
+
};
39
+
40
+
let db = DatabaseConnection::new(location, pragmas)
41
+
.await
42
+
.context("Failed to initialize the actor database")?;
43
+
44
+
// Ensure WAL mode is properly set up
45
+
db.ensure_wal().await?;
46
+
47
+
// Run migrations
48
+
// TODO: make sure the migrations are populated?
49
+
db.run_migrations()
50
+
.context("Failed to run migrations on the actor database")?;
51
+
52
+
Ok(db)
53
+
}
-146
src/actor_store/db/migrations.rs
-146
src/actor_store/db/migrations.rs
···
1
-
//! Database migrations for the actor store.
2
-
use anyhow::{Context as _, Result};
3
-
4
-
use super::ActorDb;
5
-
6
-
/// Migration identifier
7
-
type Migration = fn(&ActorDb) -> Result<()>;
8
-
9
-
/// Database migrator
10
-
pub(crate) struct Migrator {
11
-
db: ActorDb,
12
-
migrations: Vec<Migration>,
13
-
}
14
-
15
-
impl Migrator {
16
-
/// Create a new migrator
17
-
pub(crate) fn new(db: ActorDb) -> Self {
18
-
Self {
19
-
db,
20
-
migrations: vec![_001_init],
21
-
}
22
-
}
23
-
24
-
/// Run all migrations
25
-
pub(crate) async fn migrate_to_latest(&self) -> Result<()> {
26
-
let past_migrations = sqlx::query!("SELECT name FROM actor_migration")
27
-
.fetch_all(&self.db.pool)
28
-
.await?;
29
-
let mut past_migration_names = past_migrations
30
-
.iter()
31
-
.map(|m| m.name.clone())
32
-
.collect::<Vec<_>>();
33
-
past_migration_names.sort();
34
-
for migration in &self.migrations {
35
-
let name = format!("{:p}", migration);
36
-
if !past_migration_names.contains(&name) {
37
-
migration(&self.db)?;
38
-
let now = chrono::Utc::now().to_rfc3339();
39
-
sqlx::query!(
40
-
"INSERT INTO actor_migration (name, appliedAt) VALUES (?, ?)",
41
-
name,
42
-
now,
43
-
)
44
-
.execute(&self.db.pool)
45
-
.await
46
-
.context("failed to insert migration record")?;
47
-
}
48
-
}
49
-
Ok(())
50
-
}
51
-
52
-
/// Run migrations and throw an error if any fail
53
-
pub(crate) async fn migrate_to_latest_or_throw(&self) -> Result<()> {
54
-
self.migrate_to_latest().await?;
55
-
self.db.ensure_wal().await?;
56
-
Ok(())
57
-
}
58
-
}
59
-
60
-
/// Initial migration to create tables
61
-
fn _001_init(db: &ActorDb) -> Result<()> {
62
-
tokio::task::block_in_place(|| {
63
-
tokio::runtime::Handle::current()
64
-
.block_on(create_tables(&db))
65
-
.context("failed to create initial tables")
66
-
})
67
-
}
68
-
69
-
/// Create the initial database tables
70
-
pub(crate) async fn create_tables(db: &ActorDb) -> Result<()> {
71
-
sqlx::query(
72
-
"
73
-
CREATE TABLE IF NOT EXISTS repo_root (
74
-
did TEXT PRIMARY KEY NOT NULL,
75
-
cid TEXT NOT NULL,
76
-
rev TEXT NOT NULL,
77
-
indexedAt TEXT NOT NULL
78
-
);
79
-
80
-
CREATE TABLE IF NOT EXISTS repo_block (
81
-
cid TEXT PRIMARY KEY NOT NULL,
82
-
repoRev TEXT NOT NULL,
83
-
size INTEGER NOT NULL,
84
-
content BLOB NOT NULL
85
-
);
86
-
87
-
CREATE TABLE IF NOT EXISTS record (
88
-
uri TEXT PRIMARY KEY NOT NULL,
89
-
cid TEXT NOT NULL,
90
-
collection TEXT NOT NULL,
91
-
rkey TEXT NOT NULL,
92
-
repoRev TEXT NOT NULL,
93
-
indexedAt TEXT NOT NULL,
94
-
takedownRef TEXT
95
-
);
96
-
97
-
CREATE TABLE IF NOT EXISTS blob (
98
-
cid TEXT PRIMARY KEY NOT NULL,
99
-
mimeType TEXT NOT NULL,
100
-
size INTEGER NOT NULL,
101
-
tempKey TEXT,
102
-
width INTEGER,
103
-
height INTEGER,
104
-
createdAt TEXT NOT NULL,
105
-
takedownRef TEXT
106
-
);
107
-
108
-
CREATE TABLE IF NOT EXISTS record_blob (
109
-
blobCid TEXT NOT NULL,
110
-
recordUri TEXT NOT NULL,
111
-
PRIMARY KEY (blobCid, recordUri)
112
-
);
113
-
114
-
CREATE TABLE IF NOT EXISTS backlink (
115
-
uri TEXT NOT NULL,
116
-
path TEXT NOT NULL,
117
-
linkTo TEXT NOT NULL,
118
-
PRIMARY KEY (uri, path)
119
-
);
120
-
121
-
CREATE TABLE IF NOT EXISTS account_pref (
122
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
123
-
name TEXT NOT NULL,
124
-
valueJson TEXT NOT NULL
125
-
);
126
-
127
-
CREATE TABLE IF NOT EXISTS actor_migration (
128
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
129
-
name TEXT NOT NULL,
130
-
appliedAt TEXT NOT NULL
131
-
);
132
-
133
-
CREATE INDEX IF NOT EXISTS idx_repo_block_repo_rev ON repo_block(repoRev, cid);
134
-
CREATE INDEX IF NOT EXISTS idx_record_cid ON record(cid);
135
-
CREATE INDEX IF NOT EXISTS idx_record_collection ON record(collection);
136
-
CREATE INDEX IF NOT EXISTS idx_record_repo_rev ON record(repoRev);
137
-
CREATE INDEX IF NOT EXISTS idx_blob_tempkey ON blob(tempKey);
138
-
CREATE INDEX IF NOT EXISTS idx_backlink_link_to ON backlink(path, linkTo);
139
-
",
140
-
)
141
-
.execute(&db.pool)
142
-
.await
143
-
.context("failed to create tables")?;
144
-
145
-
Ok(())
146
-
}
-45
src/actor_store/db/mod.rs
-45
src/actor_store/db/mod.rs
···
1
-
//! Database schema and connection management for the actor store.
2
-
3
-
pub(crate) mod migrations;
4
-
pub(crate) mod schema;
5
-
6
-
use crate::db::Database;
7
-
use anyhow::{Context as _, Result};
8
-
9
-
/// Type alias for the actor database.
10
-
pub(crate) type ActorDb = Database;
11
-
12
-
/// Gets a database connection for the actor store.
13
-
///
14
-
/// # Arguments
15
-
///
16
-
/// * `location` - The file path or URI for the SQLite database.
17
-
/// * `disable_wal_auto_checkpoint` - Whether to disable the WAL auto-checkpoint.
18
-
///
19
-
/// # Returns
20
-
///
21
-
/// A `Result` containing the `ActorDb` instance or an error.
22
-
pub async fn get_db(location: &str, disable_wal_auto_checkpoint: bool) -> Result<ActorDb> {
23
-
let pragmas = if disable_wal_auto_checkpoint {
24
-
Some(&[("wal_autocheckpoint", "0")][..])
25
-
} else {
26
-
None
27
-
};
28
-
29
-
Database::new(location, pragmas)
30
-
.await
31
-
.context("Failed to initialize the actor database")
32
-
}
33
-
34
-
/// Gets a migrator for the actor database.
35
-
///
36
-
/// # Arguments
37
-
///
38
-
/// * `db` - The actor database instance.
39
-
///
40
-
/// # Returns
41
-
///
42
-
/// A `migrations::Migrator` instance for managing database migrations.
43
-
pub fn get_migrator(db: &ActorDb) -> migrations::Migrator {
44
-
migrations::Migrator::new(db.clone())
45
-
}
-92
src/actor_store/db/schema.rs
-92
src/actor_store/db/schema.rs
···
1
-
//! Database schema definitions for the actor store.
2
-
3
-
/// Repository root information
4
-
#[derive(Debug, Clone, sqlx::FromRow)]
5
-
#[sqlx(rename_all = "camelCase")]
6
-
pub(crate) struct RepoRoot {
7
-
pub(crate) did: String,
8
-
pub(crate) cid: String,
9
-
pub(crate) rev: String,
10
-
pub(crate) indexed_at: String,
11
-
}
12
-
13
-
pub(crate) const REPO_ROOT_TABLE: &str = "repo_root";
14
-
15
-
/// Repository block (IPLD block)
16
-
#[derive(Debug, Clone, sqlx::FromRow)]
17
-
#[sqlx(rename_all = "camelCase")]
18
-
pub(crate) struct RepoBlock {
19
-
pub(crate) cid: String,
20
-
pub(crate) repo_rev: String,
21
-
pub(crate) size: i64,
22
-
pub(crate) content: Vec<u8>,
23
-
}
24
-
25
-
pub(crate) const REPO_BLOCK_TABLE: &str = "repo_block";
26
-
27
-
/// Record information
28
-
#[derive(Debug, Clone, sqlx::FromRow)]
29
-
#[sqlx(rename_all = "camelCase")]
30
-
pub(crate) struct Record {
31
-
pub(crate) uri: String,
32
-
pub(crate) cid: String,
33
-
pub(crate) collection: String,
34
-
pub(crate) rkey: String,
35
-
pub(crate) repo_rev: Option<String>,
36
-
pub(crate) indexed_at: String,
37
-
pub(crate) takedown_ref: Option<String>,
38
-
pub(crate) did: String,
39
-
}
40
-
41
-
pub(crate) const RECORD_TABLE: &str = "record";
42
-
43
-
/// Blob information
44
-
#[derive(Debug, Clone, sqlx::FromRow)]
45
-
#[sqlx(rename_all = "camelCase")]
46
-
pub(crate) struct Blob {
47
-
pub(crate) cid: String,
48
-
pub(crate) mime_type: String,
49
-
pub(crate) size: i64,
50
-
pub(crate) temp_key: Option<String>,
51
-
pub(crate) width: Option<i64>,
52
-
pub(crate) height: Option<i64>,
53
-
pub(crate) created_at: String,
54
-
pub(crate) takedown_ref: Option<String>,
55
-
pub(crate) did: String,
56
-
}
57
-
58
-
pub(crate) const BLOB_TABLE: &str = "blob";
59
-
60
-
/// Record-blob association
61
-
#[derive(Debug, Clone, sqlx::FromRow)]
62
-
#[sqlx(rename_all = "camelCase")]
63
-
pub(crate) struct RecordBlob {
64
-
pub(crate) blob_cid: String,
65
-
pub(crate) record_uri: String,
66
-
}
67
-
68
-
pub(crate) const RECORD_BLOB_TABLE: &str = "record_blob";
69
-
70
-
/// Backlink between records
71
-
#[derive(Debug, Clone, sqlx::FromRow)]
72
-
#[sqlx(rename_all = "camelCase")]
73
-
pub(crate) struct Backlink {
74
-
pub(crate) uri: String,
75
-
pub(crate) path: String,
76
-
pub(crate) link_to: String,
77
-
}
78
-
79
-
pub(crate) const BACKLINK_TABLE: &str = "backlink";
80
-
81
-
/// Account preference
82
-
#[derive(Debug, Clone, sqlx::FromRow)]
83
-
#[sqlx(rename_all = "camelCase")]
84
-
pub(crate) struct AccountPref {
85
-
pub(crate) id: i64,
86
-
pub(crate) name: String,
87
-
pub(crate) value_json: String,
88
-
}
89
-
90
-
pub(crate) const ACCOUNT_PREF_TABLE: &str = "account_pref";
91
-
92
-
pub(crate) type DatabaseSchema = sqlx::Sqlite;
+1
src/actor_store/mod.rs
+1
src/actor_store/mod.rs
+184
src/actor_store/preference.rs
+184
src/actor_store/preference.rs
···
1
+
//! Preference handling for actor store.
2
+
3
+
use anyhow::{Context as _, Result, bail};
4
+
use diesel::prelude::*;
5
+
use serde::{Deserialize, Serialize};
6
+
use serde_json::Value as JsonValue;
7
+
use std::sync::Arc;
8
+
9
+
use crate::actor_store::db::ActorDb;
10
+
11
+
/// Constants for preference-related operations
12
+
const FULL_ACCESS_ONLY_PREFS: &[&str] = &["app.bsky.actor.defs#personalDetailsPref"];
13
+
14
+
/// User preference with type information.
15
+
#[derive(Debug, Clone, Serialize, Deserialize)]
16
+
pub(crate) struct AccountPreference {
17
+
/// Type of the preference.
18
+
pub r#type: String,
19
+
/// Preference data as JSON.
20
+
pub value: JsonValue,
21
+
}
22
+
23
+
/// Handler for preference operations with both read and write capabilities.
24
+
pub(crate) struct PreferenceHandler {
25
+
/// Database connection.
26
+
pub db: ActorDb,
27
+
/// DID of the actor.
28
+
pub did: String,
29
+
}
30
+
31
+
impl PreferenceHandler {
32
+
/// Create a new preference handler.
33
+
pub(crate) fn new(db: ActorDb, did: String) -> Self {
34
+
Self { db, did }
35
+
}
36
+
37
+
/// Get preferences for a namespace.
38
+
pub(crate) async fn get_preferences(
39
+
&self,
40
+
namespace: Option<&str>,
41
+
scope: &str,
42
+
) -> Result<Vec<AccountPreference>> {
43
+
use rsky_pds::schema::pds::account_pref::dsl::*;
44
+
45
+
let did = self.did.clone();
46
+
let namespace_clone = namespace.map(|ns| ns.to_string());
47
+
let scope_clone = scope.to_string();
48
+
49
+
let prefs_res = self
50
+
.db
51
+
.run(move |conn| {
52
+
let prefs = account_pref
53
+
.filter(did.eq(&did))
54
+
.order(id.asc())
55
+
.load::<rsky_pds::models::AccountPref>(conn)
56
+
.context("Failed to fetch preferences")?;
57
+
58
+
Ok::<Vec<rsky_pds::models::AccountPref>, diesel::result::Error>(prefs)
59
+
})
60
+
.await?;
61
+
62
+
// Filter preferences based on namespace and scope
63
+
let filtered_prefs = prefs_res
64
+
.into_iter()
65
+
.filter(|pref| {
66
+
namespace_clone
67
+
.as_ref()
68
+
.map_or(true, |ns| pref_match_namespace(ns, &pref.name))
69
+
})
70
+
.filter(|pref| pref_in_scope(scope, &pref.name))
71
+
.map(|pref| -> Result<AccountPreference> {
72
+
let value_json = match pref.value_json {
73
+
Some(json) => serde_json::from_str(&json)
74
+
.context(format!("Failed to parse preference JSON for {}", pref.name))?,
75
+
None => bail!("Preference JSON is null for {}", pref.name),
76
+
};
77
+
78
+
Ok(AccountPreference {
79
+
r#type: pref.name,
80
+
value: value_json,
81
+
})
82
+
})
83
+
.collect::<Result<Vec<_>>>()?;
84
+
85
+
Ok(filtered_prefs)
86
+
}
87
+
88
+
/// Put preferences for a namespace.
89
+
pub(crate) async fn put_preferences(
90
+
&self,
91
+
values: Vec<AccountPreference>,
92
+
namespace: &str,
93
+
scope: &str,
94
+
) -> Result<()> {
95
+
// Validate all preferences match the namespace
96
+
if !values
97
+
.iter()
98
+
.all(|value| pref_match_namespace(namespace, &value.r#type))
99
+
{
100
+
bail!("Some preferences are not in the {} namespace", namespace);
101
+
}
102
+
103
+
// Validate scope permissions
104
+
let not_in_scope = values
105
+
.iter()
106
+
.filter(|val| !pref_in_scope(scope, &val.r#type))
107
+
.collect::<Vec<_>>();
108
+
109
+
if !not_in_scope.is_empty() {
110
+
bail!("Do not have authorization to set preferences");
111
+
}
112
+
113
+
let did = self.did.clone();
114
+
let namespace_str = namespace.to_string();
115
+
let scope_str = scope.to_string();
116
+
117
+
// Convert preferences to serialized form
118
+
let serialized_prefs = values
119
+
.into_iter()
120
+
.map(|pref| -> Result<(String, String)> {
121
+
let json = serde_json::to_string(&pref.value)
122
+
.context("Failed to serialize preference value")?;
123
+
Ok((pref.r#type, json))
124
+
})
125
+
.collect::<Result<Vec<_>>>()?;
126
+
127
+
// Execute transaction
128
+
self.db
129
+
.transaction(move |conn| {
130
+
use rsky_pds::schema::pds::account_pref::dsl::*;
131
+
132
+
// Find all preferences in the namespace
133
+
let namespace_pattern = format!("{}%", namespace_str);
134
+
let all_prefs = account_pref
135
+
.filter(did.eq(&did))
136
+
.filter(name.eq(&namespace_str).or(name.like(&namespace_pattern)))
137
+
.load::<rsky_pds::models::AccountPref>(conn)
138
+
.context("Failed to fetch preferences")?;
139
+
140
+
// Filter to those in scope
141
+
let all_pref_ids_in_namespace = all_prefs
142
+
.iter()
143
+
.filter(|pref| pref_match_namespace(&namespace_str, &pref.name))
144
+
.filter(|pref| pref_in_scope(&scope_str, &pref.name))
145
+
.map(|pref| pref.id)
146
+
.collect::<Vec<i32>>();
147
+
148
+
// Delete existing preferences in namespace
149
+
if !all_pref_ids_in_namespace.is_empty() {
150
+
diesel::delete(account_pref)
151
+
.filter(id.eq_any(all_pref_ids_in_namespace))
152
+
.execute(conn)
153
+
.context("Failed to delete existing preferences")?;
154
+
}
155
+
156
+
// Insert new preferences
157
+
if !serialized_prefs.is_empty() {
158
+
for (pref_type, pref_json) in serialized_prefs {
159
+
diesel::insert_into(account_pref)
160
+
.values((
161
+
did.eq(&did),
162
+
name.eq(&pref_type),
163
+
valueJson.eq(Some(&pref_json)),
164
+
))
165
+
.execute(conn)
166
+
.context("Failed to insert preference")?;
167
+
}
168
+
}
169
+
170
+
Ok(())
171
+
})
172
+
.await
173
+
}
174
+
}
175
+
176
+
/// Check if a preference matches a namespace.
177
+
pub(super) fn pref_match_namespace(namespace: &str, fullname: &str) -> bool {
178
+
fullname == namespace || fullname.starts_with(&format!("{}.", namespace))
179
+
}
180
+
181
+
/// Check if a preference is in scope.
182
+
pub(super) fn pref_in_scope(scope: &str, pref_type: &str) -> bool {
183
+
scope == "access" || !FULL_ACCESS_ONLY_PREFS.contains(&pref_type)
184
+
}
-7
src/actor_store/preference/mod.rs
-7
src/actor_store/preference/mod.rs
-62
src/actor_store/preference/reader.rs
-62
src/actor_store/preference/reader.rs
···
1
-
//! Reader for preference data in the actor store.
2
-
3
-
use anyhow::Result;
4
-
5
-
use super::super::ActorDb;
6
-
7
-
/// Reader for preference data.
8
-
pub(crate) struct PreferenceReader {
9
-
/// Database connection.
10
-
pub db: ActorDb,
11
-
}
12
-
13
-
/// User preference with type information.
14
-
#[derive(Debug, Clone, serde::Deserialize)]
15
-
pub(crate) struct AccountPreference {
16
-
/// Type of the preference.
17
-
pub r#type: String,
18
-
/// Preference data as JSON.
19
-
pub value: serde_json::Value,
20
-
}
21
-
22
-
impl PreferenceReader {
23
-
/// Create a new preference reader.
24
-
pub(crate) fn new(db: ActorDb) -> Self {
25
-
Self { db }
26
-
}
27
-
28
-
/// Get preferences for a namespace.
29
-
pub(crate) async fn get_preferences(
30
-
&self,
31
-
namespace: Option<&str>,
32
-
scope: &str,
33
-
) -> Result<Vec<AccountPreference>> {
34
-
let prefs_res = sqlx::query!("SELECT * FROM account_pref ORDER BY id")
35
-
.fetch_all(&self.db.pool)
36
-
.await?;
37
-
38
-
let prefs = prefs_res
39
-
.into_iter()
40
-
.filter(|pref| {
41
-
namespace.map_or(true, |ns| pref_match_namespace(ns, &pref.name))
42
-
&& pref_in_scope(scope, &pref.name)
43
-
})
44
-
.map(|pref| serde_json::from_str(&pref.valueJson).unwrap())
45
-
.collect();
46
-
47
-
Ok(prefs)
48
-
}
49
-
}
50
-
51
-
/// Check if a preference matches a namespace.
52
-
pub(super) fn pref_match_namespace(namespace: &str, fullname: &str) -> bool {
53
-
fullname == namespace || fullname.starts_with(&format!("{}.", namespace))
54
-
}
55
-
56
-
/// Check if a preference is in scope.
57
-
pub(super) fn pref_in_scope(scope: &str, pref_type: &str) -> bool {
58
-
scope == "access" || !FULL_ACCESS_ONLY_PREFS.contains(&pref_type)
59
-
}
60
-
61
-
/// Preferences that require full access.
62
-
pub(super) const FULL_ACCESS_ONLY_PREFS: &[&str] = &["app.bsky.actor.defs#personalDetailsPref"];
-114
src/actor_store/preference/transactor.rs
-114
src/actor_store/preference/transactor.rs
···
1
-
// src/actor_store/preference/transactor.rs
2
-
use anyhow::{Context as _, Result, bail};
3
-
4
-
use super::super::ActorDb;
5
-
6
-
use super::reader::{AccountPreference, PreferenceReader, pref_in_scope, pref_match_namespace};
7
-
8
-
/// Transactor for preference operations.
9
-
pub(crate) struct PreferenceTransactor {
10
-
/// Preference reader.
11
-
pub reader: PreferenceReader,
12
-
}
13
-
14
-
impl PreferenceTransactor {
15
-
/// Create a new preference transactor.
16
-
pub(crate) fn new(db: ActorDb) -> Self {
17
-
Self {
18
-
reader: PreferenceReader::new(db),
19
-
}
20
-
}
21
-
22
-
/// Put preferences for a namespace.
23
-
pub(crate) async fn put_preferences(
24
-
&self,
25
-
values: Vec<AccountPreference>,
26
-
namespace: &str,
27
-
scope: &str,
28
-
) -> Result<()> {
29
-
// Validate all preferences match the namespace
30
-
if !values
31
-
.iter()
32
-
.all(|value| pref_match_namespace(namespace, &value.r#type))
33
-
{
34
-
bail!("Some preferences are not in the {} namespace", namespace);
35
-
}
36
-
37
-
// Validate scope permissions
38
-
let not_in_scope = values
39
-
.iter()
40
-
.filter(|val| !pref_in_scope(scope, &val.r#type))
41
-
.collect::<Vec<_>>();
42
-
43
-
if !not_in_scope.is_empty() {
44
-
bail!("Do not have authorization to set preferences");
45
-
}
46
-
47
-
// Get current preferences
48
-
let mut tx = self
49
-
.reader
50
-
.db
51
-
.pool
52
-
.begin()
53
-
.await
54
-
.context("failed to begin transaction")?;
55
-
56
-
// Find all preferences in the namespace
57
-
let namespace_pattern = format!("{}%", namespace);
58
-
let all_prefs = sqlx::query!(
59
-
"SELECT id, name FROM account_pref WHERE name LIKE ? OR name = ?",
60
-
namespace_pattern,
61
-
namespace
62
-
)
63
-
.fetch_all(&mut *tx)
64
-
.await
65
-
.context("failed to fetch preferences")?;
66
-
67
-
// Filter to those in scope
68
-
let all_pref_ids_in_namespace = all_prefs
69
-
.iter()
70
-
.filter(|pref| pref_match_namespace(namespace, &pref.name))
71
-
.filter(|pref| pref_in_scope(scope, &pref.name))
72
-
.map(|pref| pref.id)
73
-
.collect::<Vec<i64>>();
74
-
75
-
// Delete existing preferences in namespace
76
-
if !all_pref_ids_in_namespace.is_empty() {
77
-
let placeholders = std::iter::repeat("?")
78
-
.take(all_pref_ids_in_namespace.len())
79
-
.collect::<Vec<_>>()
80
-
.join(",");
81
-
82
-
let query = format!("DELETE FROM account_pref WHERE id IN ({})", placeholders);
83
-
84
-
let mut query_builder = sqlx::query(&query);
85
-
for id in &all_pref_ids_in_namespace {
86
-
query_builder = query_builder.bind(id);
87
-
}
88
-
89
-
query_builder
90
-
.execute(&mut *tx)
91
-
.await
92
-
.context("failed to delete preferences")?;
93
-
}
94
-
95
-
// Insert new preferences
96
-
if !values.is_empty() {
97
-
for pref in values {
98
-
let value_json = serde_json::to_string(&pref.value)?;
99
-
sqlx::query!(
100
-
"INSERT INTO account_pref (name, valueJson) VALUES (?, ?)",
101
-
pref.r#type,
102
-
value_json
103
-
)
104
-
.execute(&mut *tx)
105
-
.await
106
-
.context("failed to insert preference")?;
107
-
}
108
-
}
109
-
110
-
tx.commit().await.context("failed to commit transaction")?;
111
-
112
-
Ok(())
113
-
}
114
-
}
+19
-7
src/actor_store/prepared_write.rs
+19
-7
src/actor_store/prepared_write.rs
···
1
+
use std::str::FromStr;
2
+
3
+
use cidv10::Cid as CidV10;
1
4
use rsky_repo::types::{
2
5
CommitAction, PreparedBlobRef, PreparedCreateOrUpdate, PreparedDelete, WriteOpAction,
3
6
};
···
19
22
}
20
23
}
21
24
22
-
pub fn cid(&self) -> Option<Cid> {
25
+
pub fn cid(&self) -> Option<CidV10> {
23
26
match self {
24
-
PreparedWrite::Create(w) => Some(w.cid),
25
-
PreparedWrite::Update(w) => Some(w.cid),
27
+
PreparedWrite::Create(w) => Some(CidV10::from_str(w.cid.to_string().as_str()).unwrap()),
28
+
PreparedWrite::Update(w) => Some(CidV10::from_str(w.cid.to_string().as_str()).unwrap()),
26
29
PreparedWrite::Delete(_) => None,
27
30
}
28
31
}
29
32
30
-
pub fn swap_cid(&self) -> &Option<Cid> {
33
+
pub fn swap_cid(&self) -> Option<CidV10> {
31
34
match self {
32
-
PreparedWrite::Create(w) => &w.swap_cid,
33
-
PreparedWrite::Update(w) => &w.swap_cid,
34
-
PreparedWrite::Delete(w) => &w.swap_cid,
35
+
PreparedWrite::Create(w) => w
36
+
.swap_cid
37
+
.as_ref()
38
+
.map(|cid| CidV10::from_str(cid.to_string().as_str()).unwrap()),
39
+
PreparedWrite::Update(w) => w
40
+
.swap_cid
41
+
.as_ref()
42
+
.map(|cid| CidV10::from_str(cid.to_string().as_str()).unwrap()),
43
+
PreparedWrite::Delete(w) => w
44
+
.swap_cid
45
+
.as_ref()
46
+
.map(|cid| CidV10::from_str(cid.to_string().as_str()).unwrap()),
35
47
}
36
48
}
37
49
+845
src/actor_store/record.rs
+845
src/actor_store/record.rs
···
1
+
//! Record storage and retrieval for the actor store.
2
+
3
+
use anyhow::{Context as _, Result, bail};
4
+
use atrium_api::com::atproto::admin::defs::StatusAttr;
5
+
use atrium_repo::Cid;
6
+
use diesel::associations::HasTable;
7
+
use diesel::prelude::*;
8
+
use rsky_pds::models::{Backlink, Record};
9
+
use rsky_pds::schema::pds::repo_block::dsl::repo_block;
10
+
use rsky_pds::schema::pds::{backlink, record};
11
+
use rsky_repo::types::WriteOpAction;
12
+
use rsky_syntax::aturi::AtUri;
13
+
use std::str::FromStr;
14
+
15
+
use crate::actor_store::blob::BlobStorePlaceholder;
16
+
use crate::actor_store::db::ActorDb;
17
+
18
+
/// Combined handler for record operations with both read and write capabilities.
19
+
pub(crate) struct RecordHandler {
20
+
/// Database connection.
21
+
pub db: ActorDb,
22
+
/// DID of the actor.
23
+
pub did: String,
24
+
/// Blob store for handling blobs.
25
+
pub blobstore: Option<BlobStorePlaceholder>,
26
+
}
27
+
28
+
/// Record descriptor containing URI, path, and CID.
29
+
pub(crate) struct RecordDescript {
30
+
/// Record URI.
31
+
pub uri: String,
32
+
/// Record path.
33
+
pub path: String,
34
+
/// Record CID.
35
+
pub cid: Cid,
36
+
}
37
+
38
+
/// Record data with values.
39
+
#[derive(Debug, Clone)]
40
+
pub(crate) struct RecordData {
41
+
/// Record URI.
42
+
pub uri: String,
43
+
/// Record CID.
44
+
pub cid: String,
45
+
/// Record value as JSON.
46
+
pub value: serde_json::Value,
47
+
/// When the record was indexed.
48
+
pub indexedAt: String,
49
+
/// Reference for takedown, if any.
50
+
pub takedownRef: Option<String>,
51
+
}
52
+
53
+
/// Options for listing records in a collection.
54
+
#[derive(Debug, Clone)]
55
+
pub(crate) struct ListRecordsOptions {
56
+
/// Collection to list records from.
57
+
pub collection: String,
58
+
/// Maximum number of records to return.
59
+
pub limit: i64,
60
+
/// Whether to reverse the sort order.
61
+
pub reverse: bool,
62
+
/// Cursor for pagination.
63
+
pub cursor: Option<String>,
64
+
/// Start key (deprecated).
65
+
pub rkey_start: Option<String>,
66
+
/// End key (deprecated).
67
+
pub rkey_end: Option<String>,
68
+
/// Whether to include soft-deleted records.
69
+
pub include_soft_deleted: bool,
70
+
}
71
+
72
+
impl RecordHandler {
73
+
/// Create a new record handler.
74
+
pub(crate) fn new(db: ActorDb, did: String) -> Self {
75
+
Self {
76
+
db,
77
+
did,
78
+
blobstore: None,
79
+
}
80
+
}
81
+
82
+
/// Create a new record handler with blobstore support.
83
+
pub(crate) fn new_with_blobstore(
84
+
db: ActorDb,
85
+
blobstore: BlobStorePlaceholder,
86
+
did: String,
87
+
) -> Self {
88
+
Self {
89
+
db,
90
+
did,
91
+
blobstore: Some(blobstore),
92
+
}
93
+
}
94
+
95
+
/// Count the total number of records.
96
+
pub(crate) async fn record_count(&self) -> Result<i64> {
97
+
let did = self.did.clone();
98
+
99
+
self.db
100
+
.run(move |conn| {
101
+
use rsky_pds::schema::pds::record::dsl::*;
102
+
103
+
record.filter(did.eq(&did)).count().get_result(conn)
104
+
})
105
+
.await
106
+
}
107
+
108
+
/// List all records.
109
+
pub(crate) async fn list_all(&self) -> Result<Vec<RecordDescript>> {
110
+
let did = self.did.clone();
111
+
let mut records = Vec::new();
112
+
let mut current_cursor = Some("".to_string());
113
+
114
+
while let Some(cursor) = current_cursor.take() {
115
+
let cursor_clone = cursor.clone();
116
+
let did_clone = did.clone();
117
+
118
+
let rows = self
119
+
.db
120
+
.run(move |conn| {
121
+
use rsky_pds::schema::pds::record::dsl::*;
122
+
123
+
record
124
+
.filter(did.eq(&did_clone))
125
+
.filter(uri.gt(&cursor_clone))
126
+
.order(uri.asc())
127
+
.limit(1000)
128
+
.select((uri, cid))
129
+
.load::<(String, String)>(conn)
130
+
})
131
+
.await?;
132
+
133
+
for (uri_str, cid_str) in &rows {
134
+
let uri = uri_str.clone();
135
+
let parts: Vec<&str> = uri.rsplitn(2, '/').collect();
136
+
let path = if parts.len() == 2 {
137
+
format!("{}/{}", parts[1], parts[0])
138
+
} else {
139
+
uri.clone()
140
+
};
141
+
142
+
match Cid::from_str(&cid_str) {
143
+
Ok(cid) => records.push(RecordDescript { uri, path, cid }),
144
+
Err(e) => tracing::warn!("Invalid CID in database: {}", e),
145
+
}
146
+
}
147
+
148
+
if let Some(last) = rows.last() {
149
+
current_cursor = Some(last.0.clone());
150
+
} else {
151
+
break;
152
+
}
153
+
}
154
+
155
+
Ok(records)
156
+
}
157
+
158
+
/// List all collections in the repository.
159
+
pub(crate) async fn list_collections(&self) -> Result<Vec<String>> {
160
+
let did = self.did.clone();
161
+
162
+
self.db
163
+
.run(move |conn| {
164
+
use rsky_pds::schema::pds::record::dsl::*;
165
+
166
+
record
167
+
.filter(did.eq(&did))
168
+
.group_by(collection)
169
+
.select(collection)
170
+
.load::<String>(conn)
171
+
})
172
+
.await
173
+
}
174
+
175
+
/// List records for a specific collection.
176
+
pub(crate) async fn list_records_for_collection(
177
+
&self,
178
+
opts: ListRecordsOptions,
179
+
) -> Result<Vec<RecordData>> {
180
+
let did = self.did.clone();
181
+
182
+
self.db
183
+
.run(move |conn| {
184
+
// Start building the query
185
+
let mut query = record::table
186
+
.inner_join(repo_block::table.on(repo_block::cid.eq(record::cid)))
187
+
.filter(record::did.eq(&did))
188
+
.filter(record::collection.eq(&opts.collection))
189
+
.into_boxed();
190
+
191
+
// Handle soft-deleted records
192
+
if !opts.include_soft_deleted {
193
+
query = query.filter(record::takedownRef.is_null());
194
+
}
195
+
196
+
// Handle cursor-based pagination first
197
+
if let Some(cursor) = &opts.cursor {
198
+
if opts.reverse {
199
+
query = query.filter(record::rkey.gt(cursor));
200
+
} else {
201
+
query = query.filter(record::rkey.lt(cursor));
202
+
}
203
+
} else {
204
+
// Fall back to deprecated rkey-based pagination
205
+
if let Some(start) = &opts.rkey_start {
206
+
query = query.filter(record::rkey.gt(start));
207
+
}
208
+
if let Some(end) = &opts.rkey_end {
209
+
query = query.filter(record::rkey.lt(end));
210
+
}
211
+
}
212
+
213
+
// Add order and limit
214
+
if opts.reverse {
215
+
query = query.order(record::rkey.asc());
216
+
} else {
217
+
query = query.order(record::rkey.desc());
218
+
}
219
+
220
+
query = query.limit(opts.limit);
221
+
222
+
// Execute the query
223
+
let results = query
224
+
.select((
225
+
record::uri,
226
+
record::cid,
227
+
record::indexedAt,
228
+
record::takedownRef,
229
+
repo_block::content,
230
+
))
231
+
.load::<(String, String, String, Option<String>, Vec<u8>)>(conn)?;
232
+
233
+
// Convert results to RecordData
234
+
let records = results
235
+
.into_iter()
236
+
.map(|(uri, cid, indexedAt, takedownRef, content)| {
237
+
let value = serde_json::from_slice(&content)
238
+
.with_context(|| format!("Failed to decode record {}", cid))?;
239
+
240
+
Ok(RecordData {
241
+
uri,
242
+
cid,
243
+
value,
244
+
indexedAt,
245
+
takedownRef,
246
+
})
247
+
})
248
+
.collect::<Result<Vec<_>>>()?;
249
+
250
+
Ok(records)
251
+
})
252
+
.await
253
+
}
254
+
255
+
/// Get a specific record by URI.
256
+
pub(crate) async fn get_record(
257
+
&self,
258
+
uri: &AtUri,
259
+
cid: Option<&str>,
260
+
include_soft_deleted: bool,
261
+
) -> Result<Option<RecordData>> {
262
+
let did = self.did.clone();
263
+
let uri_str = uri.to_string();
264
+
let cid_opt = cid.map(|c| c.to_string());
265
+
266
+
self.db
267
+
.run(move |conn| {
268
+
let mut query = record::table
269
+
.inner_join(repo_block::table.on(repo_block::cid.eq(record::cid)))
270
+
.filter(record::did.eq(&did))
271
+
.filter(record::uri.eq(&uri_str))
272
+
.into_boxed();
273
+
274
+
if !include_soft_deleted {
275
+
query = query.filter(record::takedownRef.is_null());
276
+
}
277
+
278
+
if let Some(cid_val) = cid_opt {
279
+
query = query.filter(record::cid.eq(cid_val));
280
+
}
281
+
282
+
let result = query
283
+
.select((
284
+
record::uri,
285
+
record::cid,
286
+
record::indexedAt,
287
+
record::takedownRef,
288
+
repo_block::content,
289
+
))
290
+
.first::<(String, String, String, Option<String>, Vec<u8>)>(conn)
291
+
.optional()?;
292
+
293
+
if let Some((uri, cid, indexedAt, takedownRef, content)) = result {
294
+
let value = serde_json::from_slice(&content)
295
+
.with_context(|| format!("Failed to decode record {}", cid))?;
296
+
297
+
Ok(Some(RecordData {
298
+
uri,
299
+
cid,
300
+
value,
301
+
indexedAt,
302
+
takedownRef,
303
+
}))
304
+
} else {
305
+
Ok(None)
306
+
}
307
+
})
308
+
.await
309
+
}
310
+
311
+
/// Check if a record exists.
312
+
pub(crate) async fn has_record(
313
+
&self,
314
+
uri: &str,
315
+
cid: Option<&str>,
316
+
include_soft_deleted: bool,
317
+
) -> Result<bool> {
318
+
let did = self.did.clone();
319
+
let uri_str = uri.to_string();
320
+
let cid_opt = cid.map(|c| c.to_string());
321
+
322
+
self.db
323
+
.run(move |conn| {
324
+
let mut query = record::table
325
+
.filter(record::did.eq(&did))
326
+
.filter(record::uri.eq(&uri_str))
327
+
.into_boxed();
328
+
329
+
if !include_soft_deleted {
330
+
query = query.filter(record::takedownRef.is_null());
331
+
}
332
+
333
+
if let Some(cid_val) = cid_opt {
334
+
query = query.filter(record::cid.eq(cid_val));
335
+
}
336
+
337
+
let exists = query
338
+
.select(record::uri)
339
+
.first::<String>(conn)
340
+
.optional()?
341
+
.is_some();
342
+
343
+
Ok(exists)
344
+
})
345
+
.await
346
+
}
347
+
348
+
/// Get the takedown status of a record.
349
+
pub(crate) async fn get_record_takedown_status(&self, uri: &str) -> Result<Option<StatusAttr>> {
350
+
let did = self.did.clone();
351
+
let uri_str = uri.to_string();
352
+
353
+
self.db
354
+
.run(move |conn| {
355
+
let result = record::table
356
+
.filter(record::did.eq(&did))
357
+
.filter(record::uri.eq(&uri_str))
358
+
.select(record::takedownRef)
359
+
.first::<Option<String>>(conn)
360
+
.optional()?;
361
+
362
+
match result {
363
+
Some(takedown) => match takedown {
364
+
Some(takedownRef) => Ok(Some(StatusAttr {
365
+
applied: true,
366
+
r#ref: Some(takedownRef),
367
+
})),
368
+
None => Ok(Some(StatusAttr {
369
+
applied: false,
370
+
r#ref: None,
371
+
})),
372
+
},
373
+
None => Ok(None),
374
+
}
375
+
})
376
+
.await
377
+
}
378
+
379
+
/// Get the current CID for a record URI.
380
+
pub(crate) async fn get_current_record_cid(&self, uri: &str) -> Result<Option<Cid>> {
381
+
let did = self.did.clone();
382
+
let uri_str = uri.to_string();
383
+
384
+
self.db
385
+
.run(move |conn| {
386
+
let result = record::table
387
+
.filter(record::did.eq(&did))
388
+
.filter(record::uri.eq(&uri_str))
389
+
.select(record::cid)
390
+
.first::<String>(conn)
391
+
.optional()?;
392
+
393
+
match result {
394
+
Some(cid_str) => {
395
+
let cid = Cid::from_str(&cid_str)?;
396
+
Ok(Some(cid))
397
+
}
398
+
None => Ok(None),
399
+
}
400
+
})
401
+
.await
402
+
}
403
+
404
+
/// Get backlinks for a record.
405
+
pub(crate) async fn get_record_backlinks(
406
+
&self,
407
+
collection: &str,
408
+
path: &str,
409
+
linkTo: &str,
410
+
) -> Result<Vec<Record>> {
411
+
let did = self.did.clone();
412
+
let collection_str = collection.to_string();
413
+
let path_str = path.to_string();
414
+
let linkTo_str = linkTo.to_string();
415
+
416
+
self.db
417
+
.run(move |conn| {
418
+
backlink::table
419
+
.inner_join(record::table.on(backlink::uri.eq(record::uri)))
420
+
.filter(backlink::path.eq(&path_str))
421
+
.filter(backlink::linkTo.eq(&linkTo_str))
422
+
.filter(record::collection.eq(&collection_str))
423
+
.filter(record::did.eq(&did))
424
+
.select(Record::as_select())
425
+
.load::<Record>(conn)
426
+
})
427
+
.await
428
+
}
429
+
430
+
/// Get backlink conflicts for a record.
431
+
pub(crate) async fn get_backlink_conflicts(
432
+
&self,
433
+
uri: &AtUri,
434
+
record: &serde_json::Value,
435
+
) -> Result<Vec<String>> {
436
+
let backlinks = get_backlinks(uri, record)?;
437
+
if backlinks.is_empty() {
438
+
return Ok(Vec::new());
439
+
}
440
+
441
+
let did = self.did.clone();
442
+
let uri_collection = uri.get_collection().to_string();
443
+
let mut conflicts = Vec::new();
444
+
445
+
for backlink in backlinks {
446
+
let path_str = backlink.path.clone();
447
+
let linkTo_str = backlink.linkTo.clone();
448
+
449
+
let results = self
450
+
.db
451
+
.run(move |conn| {
452
+
backlink::table
453
+
.inner_join(record::table.on(backlink::uri.eq(record::uri)))
454
+
.filter(backlink::path.eq(&path_str))
455
+
.filter(backlink::linkTo.eq(&linkTo_str))
456
+
.filter(record::collection.eq(&uri_collection))
457
+
.filter(record::did.eq(&did))
458
+
.select(record::uri)
459
+
.load::<String>(conn)
460
+
})
461
+
.await?;
462
+
463
+
conflicts.extend(results);
464
+
}
465
+
466
+
Ok(conflicts)
467
+
}
468
+
469
+
/// List existing blocks in the repository.
470
+
pub(crate) async fn list_existing_blocks(&self) -> Result<Vec<Cid>> {
471
+
let did = self.did.clone();
472
+
let mut blocks = Vec::new();
473
+
let mut current_cursor = Some("".to_string());
474
+
475
+
while let Some(cursor) = current_cursor.take() {
476
+
let cursor_clone = cursor.clone();
477
+
let did_clone = did.clone();
478
+
479
+
let rows = self
480
+
.db
481
+
.run(move |conn| {
482
+
use rsky_pds::schema::pds::repo_block::dsl::*;
483
+
484
+
repo_block
485
+
.filter(did.eq(&did_clone))
486
+
.filter(cid.gt(&cursor_clone))
487
+
.order(cid.asc())
488
+
.limit(1000)
489
+
.select(cid)
490
+
.load::<String>(conn)
491
+
})
492
+
.await?;
493
+
494
+
for cid_str in &rows {
495
+
match Cid::from_str(cid_str) {
496
+
Ok(cid) => blocks.push(cid),
497
+
Err(e) => tracing::warn!("Invalid CID in database: {}", e),
498
+
}
499
+
}
500
+
501
+
if let Some(last) = rows.last() {
502
+
current_cursor = Some(last.clone());
503
+
} else {
504
+
break;
505
+
}
506
+
}
507
+
508
+
Ok(blocks)
509
+
}
510
+
511
+
/// Get the profile record for this repository
512
+
pub(crate) async fn get_profile_record(&self) -> Result<Option<serde_json::Value>> {
513
+
let did = self.did.clone();
514
+
515
+
self.db
516
+
.run(move |conn| {
517
+
let result = record::table
518
+
.inner_join(repo_block::table.on(repo_block::cid.eq(record::cid)))
519
+
.filter(record::did.eq(&did))
520
+
.filter(record::collection.eq("app.bsky.actor.profile"))
521
+
.filter(record::rkey.eq("self"))
522
+
.select(repo_block::content)
523
+
.first::<Vec<u8>>(conn)
524
+
.optional()?;
525
+
526
+
if let Some(content) = result {
527
+
let value = serde_json::from_slice(&content)
528
+
.context("Failed to decode profile record")?;
529
+
Ok(Some(value))
530
+
} else {
531
+
Ok(None)
532
+
}
533
+
})
534
+
.await
535
+
}
536
+
537
+
/// Get records created or updated since a specific revision
538
+
pub(crate) async fn get_records_since_rev(&self, rev: &str) -> Result<Vec<RecordData>> {
539
+
let did = self.did.clone();
540
+
let rev_str = rev.to_string();
541
+
542
+
// First check if the revision exists
543
+
let exists = self
544
+
.db
545
+
.run({
546
+
let did_clone = did.clone();
547
+
let rev_clone = rev_str.clone();
548
+
549
+
move |conn| {
550
+
record::table
551
+
.filter(record::did.eq(&did_clone))
552
+
.filter(record::repoRev.le(&rev_clone))
553
+
.count()
554
+
.get_result::<i64>(conn)
555
+
.map(|count| count > 0)
556
+
}
557
+
})
558
+
.await?;
559
+
560
+
if !exists {
561
+
// No records before this revision - possible account migration case
562
+
return Ok(Vec::new());
563
+
}
564
+
565
+
// Get records since the revision
566
+
self.db
567
+
.run(move |conn| {
568
+
let results = record::table
569
+
.inner_join(repo_block::table.on(repo_block::cid.eq(record::cid)))
570
+
.filter(record::did.eq(&did))
571
+
.filter(record::repoRev.gt(&rev_str))
572
+
.order(record::repoRev.asc())
573
+
.limit(10)
574
+
.select((
575
+
record::uri,
576
+
record::cid,
577
+
record::indexedAt,
578
+
repo_block::content,
579
+
))
580
+
.load::<(String, String, String, Vec<u8>)>(conn)?;
581
+
582
+
let records = results
583
+
.into_iter()
584
+
.map(|(uri, cid, indexedAt, content)| {
585
+
let value = serde_json::from_slice(&content)
586
+
.with_context(|| format!("Failed to decode record {}", cid))?;
587
+
588
+
Ok(RecordData {
589
+
uri,
590
+
cid,
591
+
value,
592
+
indexedAt,
593
+
takedownRef: None, // Not included in the query
594
+
})
595
+
})
596
+
.collect::<Result<Vec<_>>>()?;
597
+
598
+
Ok(records)
599
+
})
600
+
.await
601
+
}
602
+
603
+
// Transactor methods
604
+
// -----------------
605
+
606
+
/// Index a record in the database.
607
+
pub(crate) async fn index_record(
608
+
&self,
609
+
uri: AtUri,
610
+
cid: Cid,
611
+
record: Option<&serde_json::Value>,
612
+
action: WriteOpAction,
613
+
repoRev: &str,
614
+
timestamp: Option<String>,
615
+
) -> Result<()> {
616
+
let uri_str = uri.to_string();
617
+
tracing::debug!("Indexing record {}", uri_str);
618
+
619
+
if !uri_str.starts_with("at://did:") {
620
+
return Err(anyhow::anyhow!("Expected indexed URI to contain DID"));
621
+
}
622
+
623
+
let collection = uri.get_collection().to_string();
624
+
let rkey = uri.get_rkey().to_string();
625
+
626
+
if collection.is_empty() {
627
+
return Err(anyhow::anyhow!(
628
+
"Expected indexed URI to contain a collection"
629
+
));
630
+
} else if rkey.is_empty() {
631
+
return Err(anyhow::anyhow!(
632
+
"Expected indexed URI to contain a record key"
633
+
));
634
+
}
635
+
636
+
let cid_str = cid.to_string();
637
+
let now = timestamp.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
638
+
let did = self.did.clone();
639
+
let repoRev = repoRev.to_string();
640
+
641
+
// Create the record for database insertion
642
+
let record_values = (
643
+
record::did.eq(&did),
644
+
record::uri.eq(&uri_str),
645
+
record::cid.eq(&cid_str),
646
+
record::collection.eq(&collection),
647
+
record::rkey.eq(&rkey),
648
+
record::repoRev.eq(&repoRev),
649
+
record::indexedAt.eq(&now),
650
+
);
651
+
652
+
self.db
653
+
.transaction(move |conn| {
654
+
// Track current version of record
655
+
diesel::insert_into(record::table)
656
+
.values(&record_values)
657
+
.on_conflict(record::uri)
658
+
.do_update()
659
+
.set((
660
+
record::cid.eq(&cid_str),
661
+
record::repoRev.eq(&repoRev),
662
+
record::indexedAt.eq(&now),
663
+
))
664
+
.execute(conn)
665
+
.context("Failed to insert/update record")?;
666
+
667
+
// Maintain backlinks if record is provided
668
+
if let Some(record_value) = record {
669
+
let backlinks = get_backlinks(&uri, record_value)?;
670
+
671
+
if action == WriteOpAction::Update {
672
+
// On update, clear old backlinks first
673
+
diesel::delete(backlink::table)
674
+
.filter(backlink::uri.eq(&uri_str))
675
+
.execute(conn)
676
+
.context("Failed to delete existing backlinks")?;
677
+
}
678
+
679
+
if !backlinks.is_empty() {
680
+
// Insert all backlinks at once
681
+
let backlink_values: Vec<_> = backlinks
682
+
.into_iter()
683
+
.map(|backlink| {
684
+
(
685
+
backlink::uri.eq(&uri_str),
686
+
backlink::path.eq(&backlink.path),
687
+
backlink::linkTo.eq(&backlink.linkTo),
688
+
)
689
+
})
690
+
.collect();
691
+
692
+
diesel::insert_into(backlink::table)
693
+
.values(&backlink_values)
694
+
.on_conflict_do_nothing()
695
+
.execute(conn)
696
+
.context("Failed to insert backlinks")?;
697
+
}
698
+
}
699
+
700
+
tracing::info!("Indexed record {}", uri_str);
701
+
Ok(())
702
+
})
703
+
.await
704
+
}
705
+
706
+
/// Delete a record from the database.
707
+
pub(crate) async fn delete_record(&self, uri: &AtUri) -> Result<()> {
708
+
let uri_str = uri.to_string();
709
+
tracing::debug!("Deleting indexed record {}", uri_str);
710
+
711
+
self.db
712
+
.transaction(move |conn| {
713
+
// Delete from record table
714
+
diesel::delete(record::table)
715
+
.filter(record::uri.eq(&uri_str))
716
+
.execute(conn)
717
+
.context("Failed to delete record")?;
718
+
719
+
// Delete from backlink table
720
+
diesel::delete(backlink::table)
721
+
.filter(backlink::uri.eq(&uri_str))
722
+
.execute(conn)
723
+
.context("Failed to delete record backlinks")?;
724
+
725
+
tracing::info!("Deleted indexed record {}", uri_str);
726
+
Ok(())
727
+
})
728
+
.await
729
+
}
730
+
731
+
/// Remove backlinks for a URI.
732
+
pub(crate) async fn remove_backlinks_by_uri(&self, uri: &str) -> Result<()> {
733
+
let uri_str = uri.to_string();
734
+
735
+
self.db
736
+
.run(move |conn| {
737
+
diesel::delete(backlink::table)
738
+
.filter(backlink::uri.eq(&uri_str))
739
+
.execute(conn)
740
+
.context("Failed to remove backlinks")?;
741
+
742
+
Ok(())
743
+
})
744
+
.await
745
+
}
746
+
747
+
/// Add backlinks to the database.
748
+
pub(crate) async fn add_backlinks(&self, backlinks: Vec<Backlink>) -> Result<()> {
749
+
if backlinks.is_empty() {
750
+
return Ok(());
751
+
}
752
+
753
+
self.db
754
+
.run(move |conn| {
755
+
let backlink_values: Vec<_> = backlinks
756
+
.into_iter()
757
+
.map(|backlink| {
758
+
(
759
+
backlink::uri.eq(&backlink.uri),
760
+
backlink::path.eq(&backlink.path),
761
+
backlink::linkTo.eq(&backlink.linkTo),
762
+
)
763
+
})
764
+
.collect();
765
+
766
+
diesel::insert_into(backlink::table)
767
+
.values(&backlink_values)
768
+
.on_conflict_do_nothing()
769
+
.execute(conn)
770
+
.context("Failed to add backlinks")?;
771
+
772
+
Ok(())
773
+
})
774
+
.await
775
+
}
776
+
777
+
/// Update the takedown status of a record.
778
+
pub(crate) async fn update_record_takedown_status(
779
+
&self,
780
+
uri: &AtUri,
781
+
takedown: StatusAttr,
782
+
) -> Result<()> {
783
+
let uri_str = uri.to_string();
784
+
let did = self.did.clone();
785
+
let takedownRef = if takedown.applied {
786
+
takedown
787
+
.r#ref
788
+
.or_else(|| Some(chrono::Utc::now().to_rfc3339()))
789
+
} else {
790
+
None
791
+
};
792
+
793
+
self.db
794
+
.run(move |conn| {
795
+
diesel::update(record::table)
796
+
.filter(record::did.eq(&did))
797
+
.filter(record::uri.eq(&uri_str))
798
+
.set(record::takedownRef.eq(takedownRef))
799
+
.execute(conn)
800
+
.context("Failed to update record takedown status")?;
801
+
802
+
Ok(())
803
+
})
804
+
.await
805
+
}
806
+
}
807
+
808
+
/// Extract backlinks from a record.
809
+
pub(super) fn get_backlinks(uri: &AtUri, record: &serde_json::Value) -> Result<Vec<Backlink>> {
810
+
let mut backlinks = Vec::new();
811
+
812
+
// Check for record type
813
+
if let Some(record_type) = record.get("$type").and_then(|t| t.as_str()) {
814
+
// Handle follow and block records
815
+
if record_type == "app.bsky.graph.follow" || record_type == "app.bsky.graph.block" {
816
+
if let Some(subject) = record.get("subject").and_then(|s| s.as_str()) {
817
+
// Verify it's a valid DID
818
+
if subject.starts_with("did:") {
819
+
backlinks.push(Backlink {
820
+
uri: uri.to_string(),
821
+
path: "subject".to_string(),
822
+
linkTo: subject.to_string(),
823
+
});
824
+
}
825
+
}
826
+
}
827
+
// Handle like and repost records
828
+
else if record_type == "app.bsky.feed.like" || record_type == "app.bsky.feed.repost" {
829
+
if let Some(subject) = record.get("subject") {
830
+
if let Some(subject_uri) = subject.get("uri").and_then(|u| u.as_str()) {
831
+
// Verify it's a valid AT URI
832
+
if subject_uri.starts_with("at://") {
833
+
backlinks.push(Backlink {
834
+
uri: uri.to_string(),
835
+
path: "subject.uri".to_string(),
836
+
linkTo: subject_uri.to_string(),
837
+
});
838
+
}
839
+
}
840
+
}
841
+
}
842
+
}
843
+
844
+
Ok(backlinks)
845
+
}
-7
src/actor_store/record/mod.rs
-7
src/actor_store/record/mod.rs
-575
src/actor_store/record/reader.rs
-575
src/actor_store/record/reader.rs
···
1
-
//! Reader for record data in the actor store.
2
-
3
-
use anyhow::{Context as _, Result};
4
-
use atrium_repo::Cid;
5
-
use rsky_syntax::aturi::AtUri;
6
-
use sqlx::Row;
7
-
use std::str::FromStr;
8
-
9
-
use crate::actor_store::{ActorDb, db::schema::Backlink};
10
-
11
-
/// Reader for record data.
12
-
pub(crate) struct RecordReader {
13
-
/// Database connection.
14
-
pub db: ActorDb,
15
-
}
16
-
17
-
/// Record descriptor containing URI, path, and CID.
18
-
pub(crate) struct RecordDescript {
19
-
/// Record URI.
20
-
pub uri: String,
21
-
/// Record path.
22
-
pub path: String,
23
-
/// Record CID.
24
-
pub cid: Cid,
25
-
}
26
-
27
-
/// Record data with values.
28
-
#[derive(Debug, Clone)]
29
-
pub(crate) struct RecordData {
30
-
/// Record URI.
31
-
pub uri: String,
32
-
/// Record CID.
33
-
pub cid: String,
34
-
/// Record value as JSON.
35
-
pub value: serde_json::Value,
36
-
/// When the record was indexed.
37
-
pub indexed_at: String,
38
-
/// Reference for takedown, if any.
39
-
pub takedown_ref: Option<String>,
40
-
}
41
-
42
-
/// Options for listing records in a collection.
43
-
#[derive(Debug, Clone)]
44
-
pub(crate) struct ListRecordsOptions {
45
-
/// Collection to list records from.
46
-
pub collection: String,
47
-
/// Maximum number of records to return.
48
-
pub limit: i64,
49
-
/// Whether to reverse the sort order.
50
-
pub reverse: bool,
51
-
/// Cursor for pagination.
52
-
pub cursor: Option<String>,
53
-
/// Start key (deprecated).
54
-
pub rkey_start: Option<String>,
55
-
/// End key (deprecated).
56
-
pub rkey_end: Option<String>,
57
-
/// Whether to include soft-deleted records.
58
-
pub include_soft_deleted: bool,
59
-
}
60
-
61
-
impl RecordReader {
62
-
/// Create a new record reader.
63
-
pub(crate) fn new(db: ActorDb) -> Self {
64
-
Self { db }
65
-
}
66
-
67
-
/// Count the total number of records.
68
-
pub(crate) async fn record_count(&self) -> Result<i64> {
69
-
let result = sqlx::query!(r#"SELECT COUNT(*) as count FROM record"#)
70
-
.fetch_one(&self.db.pool)
71
-
.await
72
-
.context("failed to count records")?;
73
-
74
-
Ok(result.count)
75
-
}
76
-
77
-
/// List all records.
78
-
pub(crate) async fn list_all(&self) -> Result<Vec<RecordDescript>> {
79
-
let mut records = Vec::new();
80
-
let mut cursor = Some("".to_string());
81
-
82
-
while let Some(current_cursor) = cursor.take() {
83
-
let rows = sqlx::query!(
84
-
"SELECT uri, cid FROM record WHERE uri > ? ORDER BY uri ASC LIMIT 1000",
85
-
current_cursor
86
-
)
87
-
.fetch_all(&self.db.pool)
88
-
.await
89
-
.context("failed to fetch records")?;
90
-
91
-
for row in &rows {
92
-
let uri = row.uri.clone();
93
-
let parts: Vec<&str> = uri.rsplitn(2, '/').collect();
94
-
let path = if parts.len() == 2 {
95
-
format!("{}/{}", parts[1], parts[0])
96
-
} else {
97
-
uri.clone()
98
-
};
99
-
100
-
match Cid::from_str(&row.cid) {
101
-
Ok(cid) => records.push(RecordDescript { uri, path, cid }),
102
-
Err(e) => tracing::warn!("Invalid CID in database: {}", e),
103
-
}
104
-
}
105
-
106
-
if let Some(last) = rows.last() {
107
-
cursor = Some(last.uri.clone());
108
-
}
109
-
}
110
-
111
-
Ok(records)
112
-
}
113
-
114
-
/// List all collections in the repository.
115
-
pub(crate) async fn list_collections(&self) -> Result<Vec<String>> {
116
-
let rows = sqlx::query!("SELECT collection FROM record GROUP BY collection")
117
-
.fetch_all(&self.db.pool)
118
-
.await
119
-
.context("failed to list collections")?;
120
-
121
-
Ok(rows.into_iter().map(|row| row.collection).collect())
122
-
}
123
-
124
-
/// List records for a specific collection.
125
-
pub(crate) async fn list_records_for_collection(
126
-
&self,
127
-
opts: ListRecordsOptions,
128
-
) -> Result<Vec<RecordData>> {
129
-
let mut query = sqlx::QueryBuilder::new(
130
-
"SELECT r.uri, r.cid, r.indexed_at, r.takedown_ref, b.content
131
-
FROM record r
132
-
INNER JOIN repo_block b ON b.cid = r.cid
133
-
WHERE r.collection = ",
134
-
);
135
-
136
-
query.push_bind(&opts.collection);
137
-
138
-
if !opts.include_soft_deleted {
139
-
query.push(" AND r.takedown_ref IS NULL");
140
-
}
141
-
142
-
// Handle cursor-based pagination first
143
-
if let Some(cursor) = &opts.cursor {
144
-
if opts.reverse {
145
-
query.push(" AND r.rkey > ");
146
-
} else {
147
-
query.push(" AND r.rkey < ");
148
-
}
149
-
query.push_bind(cursor);
150
-
} else {
151
-
// Fall back to deprecated rkey-based pagination
152
-
if let Some(start) = &opts.rkey_start {
153
-
query.push(" AND r.rkey > ");
154
-
query.push_bind(start);
155
-
}
156
-
if let Some(end) = &opts.rkey_end {
157
-
query.push(" AND r.rkey < ");
158
-
query.push_bind(end);
159
-
}
160
-
}
161
-
162
-
// Add order and limit
163
-
if opts.reverse {
164
-
query.push(" ORDER BY r.rkey ASC");
165
-
} else {
166
-
query.push(" ORDER BY r.rkey DESC");
167
-
}
168
-
169
-
query.push(" LIMIT ");
170
-
query.push_bind(opts.limit);
171
-
172
-
let rows = query
173
-
.build()
174
-
.fetch_all(&self.db.pool)
175
-
.await
176
-
.context("failed to list records")?;
177
-
178
-
let mut records = Vec::with_capacity(rows.len());
179
-
for row in rows {
180
-
let uri: String = row.get("uri");
181
-
let cid: String = row.get("cid");
182
-
let indexed_at: String = row.get("indexed_at");
183
-
let takedown_ref: Option<String> = row.get("takedown_ref");
184
-
let content: Vec<u8> = row.get("content");
185
-
186
-
let value = serde_json::from_slice(&content)
187
-
.context(format!("failed to decode record {}", cid))?;
188
-
189
-
records.push(RecordData {
190
-
uri,
191
-
cid,
192
-
value,
193
-
indexed_at,
194
-
takedown_ref,
195
-
});
196
-
}
197
-
198
-
Ok(records)
199
-
}
200
-
201
-
/// Get a specific record by URI.
202
-
pub(crate) async fn get_record(
203
-
&self,
204
-
uri: &str,
205
-
cid: Option<&str>,
206
-
include_soft_deleted: bool,
207
-
) -> Result<Option<RecordData>> {
208
-
let mut query = sqlx::QueryBuilder::new(
209
-
"SELECT r.uri, r.cid, r.indexed_at, r.takedown_ref, b.content
210
-
FROM record r
211
-
INNER JOIN repo_block b ON b.cid = r.cid
212
-
WHERE r.uri = ",
213
-
);
214
-
215
-
query.push_bind(uri);
216
-
217
-
if !include_soft_deleted {
218
-
query.push(" AND r.takedown_ref IS NULL");
219
-
}
220
-
221
-
if let Some(cid_str) = cid {
222
-
query.push(" AND r.cid = ");
223
-
query.push_bind(cid_str);
224
-
}
225
-
226
-
let row = query
227
-
.build()
228
-
.fetch_optional(&self.db.pool)
229
-
.await
230
-
.context("failed to fetch record")?;
231
-
232
-
if let Some(row) = row {
233
-
let uri = row.get::<String, _>("uri");
234
-
let cid = row.get::<String, _>("cid");
235
-
let indexed_at = row.get::<String, _>("indexed_at");
236
-
let takedown_ref = row.get::<Option<String>, _>("takedown_ref");
237
-
let content: Vec<u8> = row.get("content");
238
-
239
-
// Convert CBOR to Lexicon record
240
-
let value = serde_json::from_slice(&content)
241
-
.context(format!("failed to decode record {}", cid))?;
242
-
243
-
Ok(Some(RecordData {
244
-
uri,
245
-
cid,
246
-
value,
247
-
indexed_at,
248
-
takedown_ref,
249
-
}))
250
-
} else {
251
-
Ok(None)
252
-
}
253
-
}
254
-
255
-
/// Check if a record exists.
256
-
pub(crate) async fn has_record(
257
-
&self,
258
-
uri: &str,
259
-
cid: Option<&str>,
260
-
include_soft_deleted: bool,
261
-
) -> Result<bool> {
262
-
let mut query = sqlx::QueryBuilder::new("SELECT uri FROM record WHERE uri = ");
263
-
264
-
query.push_bind(uri);
265
-
266
-
if !include_soft_deleted {
267
-
query.push(" AND takedown_ref IS NULL");
268
-
}
269
-
270
-
if let Some(cid_str) = cid {
271
-
query.push(" AND cid = ");
272
-
query.push_bind(cid_str);
273
-
}
274
-
275
-
let result = query
276
-
.build()
277
-
.fetch_optional(&self.db.pool)
278
-
.await
279
-
.context("failed to check record existence")?;
280
-
281
-
Ok(result.is_some())
282
-
}
283
-
284
-
/// Get the takedown status of a record.
285
-
pub(crate) async fn get_record_takedown_status(&self, uri: &str) -> Result<Option<StatusAttr>> {
286
-
let result = sqlx::query!("SELECT takedownRef FROM record WHERE uri = ?", uri)
287
-
.fetch_optional(&self.db.pool)
288
-
.await
289
-
.context("failed to fetch takedown status")?;
290
-
291
-
match result {
292
-
Some(row) => {
293
-
if let Some(takedown_ref) = row.takedownRef {
294
-
Ok(Some(StatusAttr {
295
-
applied: true,
296
-
r#ref: Some(takedown_ref),
297
-
}))
298
-
} else {
299
-
Ok(Some(StatusAttr {
300
-
applied: false,
301
-
r#ref: None,
302
-
}))
303
-
}
304
-
}
305
-
None => Ok(None),
306
-
}
307
-
}
308
-
309
-
/// Get the current CID for a record URI.
310
-
pub(crate) async fn get_current_record_cid(&self, uri: &str) -> Result<Option<Cid>> {
311
-
let result = sqlx::query!("SELECT cid FROM record WHERE uri = ?", uri)
312
-
.fetch_optional(&self.db.pool)
313
-
.await
314
-
.context("failed to fetch record CID")?;
315
-
316
-
match result {
317
-
Some(row) => {
318
-
let cid = Cid::from_str(&row.cid)?;
319
-
Ok(Some(cid))
320
-
}
321
-
None => Ok(None),
322
-
}
323
-
}
324
-
325
-
/// Get backlinks for a record.
326
-
pub(crate) async fn get_record_backlinks(
327
-
&self,
328
-
collection: &str,
329
-
path: &str,
330
-
link_to: &str,
331
-
) -> Result<Vec<Record>> {
332
-
let rows = sqlx::query!(
333
-
r#"
334
-
SELECT r.*
335
-
FROM record r
336
-
INNER JOIN backlink b ON b.uri = r.uri
337
-
WHERE b.path = ?
338
-
AND b.linkTo = ?
339
-
AND r.collection = ?
340
-
"#,
341
-
path,
342
-
link_to,
343
-
collection
344
-
)
345
-
.fetch_all(&self.db.pool)
346
-
.await
347
-
.context("failed to fetch record backlinks")?;
348
-
349
-
let mut records = Vec::with_capacity(rows.len());
350
-
for row in rows {
351
-
records.push(Record {
352
-
uri: row.uri,
353
-
cid: row.cid,
354
-
collection: row.collection,
355
-
rkey: row.rkey,
356
-
repo_rev: Some(row.repoRev),
357
-
indexed_at: row.indexedAt,
358
-
takedown_ref: row.takedownRef,
359
-
});
360
-
}
361
-
362
-
Ok(records)
363
-
}
364
-
365
-
/// Get backlink conflicts for a record.
366
-
pub(crate) async fn get_backlink_conflicts(
367
-
&self,
368
-
uri: AtUri,
369
-
record: &serde_json::Value,
370
-
) -> Result<Vec<String>> {
371
-
let backlinks = get_backlinks(&uri, record)?;
372
-
if backlinks.is_empty() {
373
-
return Ok(Vec::new());
374
-
}
375
-
376
-
let mut conflicts = Vec::new();
377
-
for backlink in &backlinks {
378
-
let uri_collection = &uri.get_collection();
379
-
let rows = sqlx::query!(
380
-
r#"
381
-
SELECT r.uri
382
-
FROM record r
383
-
INNER JOIN backlink b ON b.uri = r.uri
384
-
WHERE b.path = ?
385
-
AND b.linkTo = ?
386
-
AND r.collection = ?
387
-
"#,
388
-
backlink.path,
389
-
backlink.link_to,
390
-
uri_collection
391
-
)
392
-
.fetch_all(&self.db.pool)
393
-
.await
394
-
.context("failed to fetch backlink conflicts")?;
395
-
396
-
for row in rows {
397
-
conflicts.push(row.uri);
398
-
}
399
-
}
400
-
401
-
Ok(conflicts)
402
-
}
403
-
404
-
/// List existing blocks in the repository.
405
-
pub(crate) async fn list_existing_blocks(&self) -> Result<Vec<Cid>> {
406
-
let mut blocks = Vec::new();
407
-
let mut cursor = Some("".to_string());
408
-
409
-
while let Some(current_cursor) = cursor.take() {
410
-
let rows = sqlx::query!(
411
-
"SELECT cid FROM repo_block WHERE cid > ? ORDER BY cid ASC LIMIT 1000",
412
-
current_cursor
413
-
)
414
-
.fetch_all(&self.db.pool)
415
-
.await
416
-
.context("failed to fetch blocks")?;
417
-
418
-
for row in &rows {
419
-
match Cid::from_str(&row.cid) {
420
-
Ok(cid) => blocks.push(cid),
421
-
Err(e) => tracing::warn!("Invalid CID in database: {}", e),
422
-
}
423
-
}
424
-
425
-
if let Some(last) = rows.last() {
426
-
cursor = Some(last.cid.clone());
427
-
}
428
-
}
429
-
430
-
Ok(blocks)
431
-
}
432
-
433
-
/// Get the profile record for this repository
434
-
pub(crate) async fn get_profile_record(&self) -> Result<Option<serde_json::Value>> {
435
-
let row = sqlx::query!(
436
-
r#"
437
-
SELECT b.content
438
-
FROM record r
439
-
LEFT JOIN repo_block b ON b.cid = r.cid
440
-
WHERE r.collection = 'app.bsky.actor.profile'
441
-
AND r.rkey = 'self'
442
-
LIMIT 1
443
-
"#
444
-
)
445
-
.fetch_optional(&self.db.pool)
446
-
.await
447
-
.context("failed to fetch profile record")?;
448
-
449
-
if let Some(row) = row {
450
-
if let Some(content) = row.content {
451
-
// Convert CBOR to JSON
452
-
let value =
453
-
serde_json::from_slice(&content).context("failed to decode profile record")?;
454
-
return Ok(Some(value));
455
-
}
456
-
}
457
-
458
-
Ok(None)
459
-
}
460
-
461
-
/// Get records created or updated since a specific revision
462
-
pub(crate) async fn get_records_since_rev(&self, rev: &str) -> Result<Vec<RecordData>> {
463
-
// First check if the revision exists
464
-
let sanity_check = sqlx::query!(
465
-
r#"SELECT repoRev FROM record WHERE repoRev <= ? LIMIT 1"#,
466
-
rev
467
-
)
468
-
.fetch_optional(&self.db.pool)
469
-
.await
470
-
.context("failed to check revision existence")?;
471
-
472
-
if sanity_check.is_none() {
473
-
// No records before this revision - possible account migration case
474
-
return Ok(Vec::new());
475
-
}
476
-
477
-
let rows = sqlx::query!(
478
-
r#"
479
-
SELECT r.uri, r.cid, r.indexedAt, b.content
480
-
FROM record r
481
-
INNER JOIN repo_block b ON b.cid = r.cid
482
-
WHERE r.repoRev > ?
483
-
ORDER BY r.repoRev ASC
484
-
LIMIT 10
485
-
"#,
486
-
rev
487
-
)
488
-
.fetch_all(&self.db.pool)
489
-
.await
490
-
.context("failed to fetch records since revision")?;
491
-
492
-
let mut records = Vec::with_capacity(rows.len());
493
-
for row in rows {
494
-
let value = serde_json::from_slice(&row.content)
495
-
.context(format!("failed to decode record {}", row.cid))?;
496
-
497
-
records.push(RecordData {
498
-
uri: row.uri,
499
-
cid: row.cid,
500
-
value,
501
-
indexed_at: row.indexedAt,
502
-
takedown_ref: None, // Not included in the query
503
-
});
504
-
}
505
-
506
-
Ok(records)
507
-
}
508
-
}
509
-
510
-
/// Database record structure.
511
-
#[derive(Debug, Clone)]
512
-
pub(crate) struct Record {
513
-
/// Record URI.
514
-
pub uri: String,
515
-
/// Record CID.
516
-
pub cid: String,
517
-
/// Record collection.
518
-
pub collection: String,
519
-
/// Record key.
520
-
pub rkey: String,
521
-
/// Repository revision.
522
-
pub repo_rev: Option<String>,
523
-
/// When the record was indexed.
524
-
pub indexed_at: String,
525
-
/// Reference for takedown, if any.
526
-
pub takedown_ref: Option<String>,
527
-
}
528
-
529
-
/// Status attribute for takedowns
530
-
#[derive(Debug, Clone)]
531
-
pub(crate) struct StatusAttr {
532
-
/// Whether the takedown is applied
533
-
pub applied: bool,
534
-
/// Reference for the takedown
535
-
pub r#ref: Option<String>,
536
-
}
537
-
538
-
/// Extract backlinks from a record.
539
-
pub(super) fn get_backlinks(uri: &AtUri, record: &serde_json::Value) -> Result<Vec<Backlink>> {
540
-
let mut backlinks = Vec::new();
541
-
542
-
// Check for record type
543
-
if let Some(record_type) = record.get("$type").and_then(|t| t.as_str()) {
544
-
// Handle follow and block records
545
-
if record_type == "app.bsky.graph.follow" || record_type == "app.bsky.graph.block" {
546
-
if let Some(subject) = record.get("subject").and_then(|s| s.as_str()) {
547
-
// Verify it's a valid DID
548
-
if subject.starts_with("did:") {
549
-
backlinks.push(Backlink {
550
-
uri: uri.to_string(),
551
-
path: "subject".to_string(),
552
-
link_to: subject.to_string(),
553
-
});
554
-
}
555
-
}
556
-
}
557
-
// Handle like and repost records
558
-
else if record_type == "app.bsky.feed.like" || record_type == "app.bsky.feed.repost" {
559
-
if let Some(subject) = record.get("subject") {
560
-
if let Some(subject_uri) = subject.get("uri").and_then(|u| u.as_str()) {
561
-
// Verify it's a valid AT URI
562
-
if subject_uri.starts_with("at://") {
563
-
backlinks.push(Backlink {
564
-
uri: uri.to_string(),
565
-
path: "subject.uri".to_string(),
566
-
link_to: subject_uri.to_string(),
567
-
});
568
-
}
569
-
}
570
-
}
571
-
}
572
-
}
573
-
574
-
Ok(backlinks)
575
-
}
-202
src/actor_store/record/transactor.rs
-202
src/actor_store/record/transactor.rs
···
1
-
//! Transactor for record operations in the actor store.
2
-
3
-
use anyhow::{Context as _, Result};
4
-
use atrium_repo::Cid;
5
-
use rsky_repo::types::WriteOpAction;
6
-
use rsky_syntax::aturi::AtUri;
7
-
8
-
use crate::actor_store::ActorDb;
9
-
use crate::actor_store::db::schema::Backlink;
10
-
use crate::actor_store::record::reader::{RecordReader, StatusAttr, get_backlinks};
11
-
use crate::repo::types::BlobStore as BlobStore;
12
-
13
-
/// Transaction handler for record operations.
14
-
pub(crate) struct RecordTransactor {
15
-
/// The record reader.
16
-
pub reader: RecordReader,
17
-
/// The blob store.
18
-
pub blobstore: BlobStore,
19
-
}
20
-
21
-
impl RecordTransactor {
22
-
/// Create a new record transactor.
23
-
pub(crate) fn new(db: ActorDb, blobstore: BlobStore) -> Self {
24
-
Self {
25
-
reader: RecordReader::new(db),
26
-
blobstore,
27
-
}
28
-
}
29
-
30
-
/// Index a record in the database.
31
-
pub(crate) async fn index_record(
32
-
&self,
33
-
uri: AtUri,
34
-
cid: Cid,
35
-
record: Option<serde_json::Value>,
36
-
action: WriteOpAction,
37
-
repo_rev: &str,
38
-
timestamp: Option<String>,
39
-
) -> Result<()> {
40
-
let uri_str = uri.to_string();
41
-
tracing::debug!("Indexing record {}", uri_str);
42
-
43
-
if !uri_str.starts_with("at://did:") {
44
-
return Err(anyhow::anyhow!("Expected indexed URI to contain DID"));
45
-
}
46
-
47
-
let collection = uri.get_collection().to_string();
48
-
let rkey = uri.get_rkey().to_string();
49
-
50
-
if collection.is_empty() {
51
-
return Err(anyhow::anyhow!(
52
-
"Expected indexed URI to contain a collection"
53
-
));
54
-
} else if rkey.is_empty() {
55
-
return Err(anyhow::anyhow!(
56
-
"Expected indexed URI to contain a record key"
57
-
));
58
-
}
59
-
60
-
let cid_str = cid.to_string();
61
-
let now = timestamp.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
62
-
63
-
// Track current version of record
64
-
_ = sqlx::query!(
65
-
r#"
66
-
INSERT INTO record (uri, cid, collection, rkey, repoRev, indexedAt)
67
-
VALUES (?, ?, ?, ?, ?, ?)
68
-
ON CONFLICT (uri) DO UPDATE SET
69
-
cid = ?,
70
-
repoRev = ?,
71
-
indexedAt = ?
72
-
"#,
73
-
uri_str,
74
-
cid_str,
75
-
collection,
76
-
rkey,
77
-
repo_rev,
78
-
now,
79
-
cid_str,
80
-
repo_rev,
81
-
now
82
-
)
83
-
.execute(&self.reader.db.pool)
84
-
.await
85
-
.context("failed to index record")?;
86
-
87
-
// Maintain backlinks if record is provided
88
-
if let Some(record_value) = record {
89
-
let backlinks = get_backlinks(&uri, &record_value)?;
90
-
91
-
if action == WriteOpAction::Update {
92
-
// On update, clear old backlinks first
93
-
self.remove_backlinks_by_uri(&uri_str).await?;
94
-
}
95
-
96
-
if !backlinks.is_empty() {
97
-
self.add_backlinks(backlinks).await?;
98
-
}
99
-
}
100
-
101
-
tracing::info!("Indexed record {}", uri_str);
102
-
Ok(())
103
-
}
104
-
105
-
/// Delete a record from the database.
106
-
pub(crate) async fn delete_record(&self, uri: &AtUri) -> Result<()> {
107
-
let uri_str = uri.to_string();
108
-
tracing::debug!("Deleting indexed record {}", uri_str);
109
-
110
-
// Delete the record and its backlinks in a transaction
111
-
let mut tx = self.reader.db.pool.begin().await?;
112
-
113
-
// Delete from record table
114
-
sqlx::query!("DELETE FROM record WHERE uri = ?", uri_str)
115
-
.execute(&mut *tx)
116
-
.await
117
-
.context("failed to delete record")?;
118
-
119
-
// Delete from backlink table
120
-
sqlx::query!("DELETE FROM backlink WHERE uri = ?", uri_str)
121
-
.execute(&mut *tx)
122
-
.await
123
-
.context("failed to delete record backlinks")?;
124
-
125
-
tx.commit().await.context("failed to commit transaction")?;
126
-
127
-
tracing::info!("Deleted indexed record {}", uri_str);
128
-
Ok(())
129
-
}
130
-
131
-
/// Remove backlinks for a URI.
132
-
pub(crate) async fn remove_backlinks_by_uri(&self, uri: &str) -> Result<()> {
133
-
sqlx::query!("DELETE FROM backlink WHERE uri = ?", uri)
134
-
.execute(&self.reader.db.pool)
135
-
.await
136
-
.context("failed to remove backlinks")?;
137
-
138
-
Ok(())
139
-
}
140
-
141
-
/// Add backlinks to the database.
142
-
pub(crate) async fn add_backlinks(&self, backlinks: Vec<Backlink>) -> Result<()> {
143
-
if backlinks.is_empty() {
144
-
return Ok(());
145
-
}
146
-
147
-
let mut query =
148
-
sqlx::QueryBuilder::new("INSERT INTO backlink (uri, path, link_to) VALUES ");
149
-
150
-
for (i, backlink) in backlinks.iter().enumerate() {
151
-
if i > 0 {
152
-
query.push(", ");
153
-
}
154
-
155
-
query
156
-
.push("(")
157
-
.push_bind(&backlink.uri)
158
-
.push(", ")
159
-
.push_bind(&backlink.path)
160
-
.push(", ")
161
-
.push_bind(&backlink.link_to)
162
-
.push(")");
163
-
}
164
-
165
-
query.push(" ON CONFLICT DO NOTHING");
166
-
167
-
query
168
-
.build()
169
-
.execute(&self.reader.db.pool)
170
-
.await
171
-
.context("failed to add backlinks")?;
172
-
173
-
Ok(())
174
-
}
175
-
176
-
/// Update the takedown status of a record.
177
-
pub(crate) async fn update_record_takedown_status(
178
-
&self,
179
-
uri: &AtUri,
180
-
takedown: StatusAttr,
181
-
) -> Result<()> {
182
-
let uri_str = uri.to_string();
183
-
let takedown_ref = if takedown.applied {
184
-
takedown
185
-
.r#ref
186
-
.or_else(|| Some(chrono::Utc::now().to_rfc3339()))
187
-
} else {
188
-
None
189
-
};
190
-
191
-
sqlx::query!(
192
-
"UPDATE record SET takedownRef = ? WHERE uri = ?",
193
-
takedown_ref,
194
-
uri_str
195
-
)
196
-
.execute(&self.reader.db.pool)
197
-
.await
198
-
.context("failed to update record takedown status")?;
199
-
200
-
Ok(())
201
-
}
202
-
}
+466
src/actor_store/repo.rs
+466
src/actor_store/repo.rs
···
1
+
//! Repository operations for actor store.
2
+
3
+
use std::str::FromStr as _;
4
+
use std::sync::Arc;
5
+
6
+
use anyhow::{Context as _, Result};
7
+
use atrium_repo::Cid;
8
+
use cidv10::Cid as CidV10;
9
+
use diesel::prelude::*;
10
+
use rsky_repo::{
11
+
block_map::BlockMap,
12
+
cid_set::CidSet,
13
+
repo::Repo,
14
+
storage::{readable_blockstore::ReadableBlockstore as _, types::RepoStorage as _},
15
+
types::{
16
+
CommitAction, CommitData, CommitDataWithOps, CommitOp, PreparedBlobRef, PreparedWrite,
17
+
WriteOpAction, write_to_op,
18
+
},
19
+
util::format_data_key,
20
+
};
21
+
use rsky_syntax::aturi::AtUri;
22
+
23
+
use super::{
24
+
ActorDb,
25
+
blob::{BackgroundQueue, BlobReader, BlobStorePlaceholder, BlobTransactor},
26
+
record::RecordHandler,
27
+
};
28
+
use crate::SigningKey;
29
+
30
+
use crate::actor_store::sql_repo::SqlRepoStorage;
31
+
32
+
/// Data for sync events.
33
+
pub(crate) struct SyncEventData {
34
+
/// The CID of the repository root.
35
+
pub cid: Cid,
36
+
/// The revision of the repository.
37
+
pub rev: String,
38
+
/// The blocks in the repository.
39
+
pub blocks: BlockMap,
40
+
}
41
+
42
+
/// Unified repository handler for the actor store with both read and write capabilities.
43
+
pub(crate) struct RepoHandler {
44
+
/// Actor DID
45
+
pub did: String,
46
+
/// Backend storage
47
+
pub storage: SqlRepoStorage,
48
+
/// BlobReader for handling blob operations
49
+
pub blob: BlobReader,
50
+
/// RecordHandler for handling record operations
51
+
pub record: RecordHandler,
52
+
/// BlobTransactor for handling blob writes
53
+
pub blob_transactor: BlobTransactor,
54
+
/// RecordHandler for handling record writes
55
+
pub record_transactor: RecordHandler,
56
+
/// Signing keypair
57
+
pub signing_key: Option<Arc<SigningKey>>,
58
+
/// Background queue for async operations
59
+
pub background_queue: BackgroundQueue,
60
+
}
61
+
62
+
impl RepoHandler {
63
+
/// Create a new repository handler with read/write capabilities.
64
+
pub(crate) fn new(
65
+
db: ActorDb,
66
+
blobstore: BlobStorePlaceholder,
67
+
did: String,
68
+
signing_key: Arc<SigningKey>,
69
+
background_queue: BackgroundQueue,
70
+
) -> Self {
71
+
// Create readers
72
+
let blob = BlobReader::new(db.clone(), blobstore.clone());
73
+
let record = RecordHandler::new(db.clone(), did.clone());
74
+
75
+
// Create storage backend with current timestamp
76
+
let now = chrono::Utc::now().to_rfc3339();
77
+
let storage = SqlRepoStorage::new(did.clone(), db.clone(), Some(now));
78
+
79
+
// Create transactors
80
+
let blob_transactor =
81
+
BlobTransactor::new(db.clone(), blobstore.clone(), background_queue.clone());
82
+
let record_transactor = RecordHandler::new(db.clone(), blobstore);
83
+
84
+
Self {
85
+
did,
86
+
storage,
87
+
blob,
88
+
record,
89
+
blob_transactor,
90
+
record_transactor,
91
+
signing_key: Some(signing_key),
92
+
background_queue,
93
+
}
94
+
}
95
+
96
+
/// Get event data for synchronization.
97
+
pub(crate) async fn get_sync_event_data(&self) -> Result<SyncEventData> {
98
+
let root = self.storage.get_root_detailed().await?;
99
+
let blocks = self
100
+
.storage
101
+
.get_blocks(vec![CidV10::from_str(&root.cid.to_string()).unwrap()])
102
+
.await?;
103
+
104
+
Ok(SyncEventData {
105
+
cid: root.cid,
106
+
rev: root.rev,
107
+
blocks: blocks.blocks,
108
+
})
109
+
}
110
+
111
+
/// Try to load repository
112
+
pub(crate) async fn maybe_load_repo(&self) -> Result<Option<Repo>> {
113
+
match self.storage.get_root().await {
114
+
Some(cid) => {
115
+
let repo = Repo::load(&self.storage, cid).await?;
116
+
Ok(Some(repo))
117
+
}
118
+
None => Ok(None),
119
+
}
120
+
}
121
+
122
+
/// Create a new repository with prepared writes
123
+
pub(crate) async fn create_repo(
124
+
&self,
125
+
writes: Vec<PreparedWrite>,
126
+
) -> Result<CommitDataWithOps> {
127
+
let signing_key = self
128
+
.signing_key
129
+
.as_ref()
130
+
.ok_or_else(|| anyhow::anyhow!("No signing key available for write operations"))?;
131
+
132
+
// Convert writes to operations
133
+
let ops = writes
134
+
.iter()
135
+
.map(|w| write_to_op(w))
136
+
.collect::<Result<Vec<_>>>()?;
137
+
138
+
// Format the initial commit
139
+
let commit = Repo::format_init_commit(&self.storage, &self.did, signing_key, ops).await?;
140
+
141
+
// Apply the commit, index the writes, and process blobs in parallel
142
+
let results = futures::future::join3(
143
+
self.storage.apply_commit(commit.clone(), Some(true)),
144
+
self.index_writes(&writes, &commit.rev),
145
+
self.blob_transactor
146
+
.process_write_blobs(&commit.rev, writes.clone()),
147
+
)
148
+
.await;
149
+
150
+
// Check for errors
151
+
results.0.context("Failed to apply commit")?;
152
+
results.1.context("Failed to index writes")?;
153
+
results.2.context("Failed to process blobs")?;
154
+
155
+
// Create commit operations
156
+
let ops = writes
157
+
.iter()
158
+
.filter_map(|w| match w {
159
+
PreparedWrite::Create(c) | PreparedWrite::Update(c) => {
160
+
let uri = AtUri::from_str(&c.uri).ok()?;
161
+
Some(CommitOp {
162
+
action: CommitAction::Create,
163
+
path: format_data_key(uri.get_collection(), uri.get_rkey()),
164
+
cid: Some(c.cid),
165
+
prev: None,
166
+
})
167
+
}
168
+
PreparedWrite::Delete(_) => None,
169
+
})
170
+
.collect();
171
+
172
+
Ok(CommitDataWithOps {
173
+
commit_data: commit,
174
+
ops,
175
+
prev_data: None,
176
+
})
177
+
}
178
+
179
+
/// Process writes to the repository
180
+
pub(crate) async fn process_writes(
181
+
&self,
182
+
writes: Vec<PreparedWrite>,
183
+
swap_commit_cid: Option<Cid>,
184
+
) -> Result<CommitDataWithOps> {
185
+
// Check write limit
186
+
if writes.len() > 200 {
187
+
return Err(anyhow::anyhow!("Too many writes. Max: 200"));
188
+
}
189
+
190
+
// Format the commit
191
+
let commit = self.format_commit(writes.clone(), swap_commit_cid).await?;
192
+
193
+
// Check commit size limit (2MB)
194
+
if commit.commit_data.relevant_blocks.byte_size()? > 2_000_000 {
195
+
return Err(anyhow::anyhow!("Too many writes. Max event size: 2MB"));
196
+
}
197
+
198
+
// Apply the commit, index the writes, and process blobs in parallel
199
+
let results = futures::future::join3(
200
+
self.storage.apply_commit(commit.commit_data.clone(), None),
201
+
self.index_writes(&writes, &commit.commit_data.rev),
202
+
self.blob_transactor
203
+
.process_write_blobs(&commit.commit_data.rev, writes),
204
+
)
205
+
.await;
206
+
207
+
// Check for errors
208
+
results.0.context("Failed to apply commit")?;
209
+
results.1.context("Failed to index writes")?;
210
+
results.2.context("Failed to process blobs")?;
211
+
212
+
Ok(commit)
213
+
}
214
+
215
+
/// Format a commit for writes
216
+
pub(crate) async fn format_commit(
217
+
&self,
218
+
writes: Vec<PreparedWrite>,
219
+
swap_commit_cid: Option<Cid>,
220
+
) -> Result<CommitDataWithOps> {
221
+
// Ensure we have a signing key
222
+
let signing_key = self
223
+
.signing_key
224
+
.as_ref()
225
+
.ok_or_else(|| anyhow::anyhow!("No signing key available for write operations"))?;
226
+
227
+
// Get current root
228
+
let curr_root = self
229
+
.storage
230
+
.get_root_detailed()
231
+
.await
232
+
.context("Failed to get repository root")?;
233
+
234
+
// Check commit swap if requested
235
+
if let Some(swap) = swap_commit_cid {
236
+
if curr_root.cid != swap {
237
+
return Err(anyhow::anyhow!(
238
+
"Bad commit swap: current={}, expected={}",
239
+
curr_root.cid,
240
+
swap
241
+
));
242
+
}
243
+
}
244
+
245
+
// Cache the current revision for better performance
246
+
self.storage.cache_rev(&curr_root.rev).await?;
247
+
248
+
// Prepare collections for tracking changes
249
+
let mut new_record_cids = Vec::new();
250
+
let mut del_and_update_uris = Vec::new();
251
+
let mut commit_ops = Vec::new();
252
+
253
+
// Process each write to build operations and gather info
254
+
for write in &writes {
255
+
match write {
256
+
PreparedWrite::Create(w) => {
257
+
new_record_cids.push(w.cid);
258
+
let uri = AtUri::from_str(&w.uri)?;
259
+
commit_ops.push(CommitOp {
260
+
action: CommitAction::Create,
261
+
path: format_data_key(uri.get_collection(), uri.get_rkey()),
262
+
cid: Some(w.cid),
263
+
prev: None,
264
+
});
265
+
266
+
// Validate swap_cid conditions
267
+
if w.swap_cid.is_some() && w.swap_cid != Some(None) {
268
+
return Err(anyhow::anyhow!(
269
+
"Bad record swap: there should be no current record for a create"
270
+
));
271
+
}
272
+
}
273
+
PreparedWrite::Update(w) => {
274
+
new_record_cids.push(w.cid);
275
+
let uri = AtUri::from_str(&w.uri)?;
276
+
del_and_update_uris.push(uri.clone());
277
+
278
+
// Get the current record if it exists
279
+
let record = self.record.get_record(&uri, None, true).await?;
280
+
let curr_record = record.as_ref().map(|r| Cid::from_str(&r.cid).unwrap());
281
+
282
+
commit_ops.push(CommitOp {
283
+
action: CommitAction::Update,
284
+
path: format_data_key(uri.get_collection(), uri.get_rkey()),
285
+
cid: Some(w.cid),
286
+
prev: curr_record,
287
+
});
288
+
289
+
// Validate swap_cid conditions
290
+
if w.swap_cid.is_some() {
291
+
if w.swap_cid == Some(None) {
292
+
return Err(anyhow::anyhow!(
293
+
"Bad record swap: there should be a current record for an update"
294
+
));
295
+
}
296
+
297
+
if let Some(Some(swap)) = w.swap_cid {
298
+
if curr_record.is_some() && curr_record != Some(swap) {
299
+
return Err(anyhow::anyhow!(
300
+
"Bad record swap: current={:?}, expected={}",
301
+
curr_record,
302
+
swap
303
+
));
304
+
}
305
+
}
306
+
}
307
+
}
308
+
PreparedWrite::Delete(w) => {
309
+
let uri = AtUri::from_str(&w.uri)?;
310
+
del_and_update_uris.push(uri.clone());
311
+
312
+
// Get the current record if it exists
313
+
let record = self.record.get_record(&uri, None, true).await?;
314
+
let curr_record = record.as_ref().map(|r| Cid::from_str(&r.cid).unwrap());
315
+
316
+
commit_ops.push(CommitOp {
317
+
action: CommitAction::Delete,
318
+
path: format_data_key(uri.get_collection(), uri.get_rkey()),
319
+
cid: None,
320
+
prev: curr_record,
321
+
});
322
+
323
+
// Validate swap_cid conditions
324
+
if w.swap_cid.is_some() {
325
+
if w.swap_cid == Some(None) {
326
+
return Err(anyhow::anyhow!(
327
+
"Bad record swap: there should be a current record for a delete"
328
+
));
329
+
}
330
+
331
+
if let Some(Some(swap)) = w.swap_cid {
332
+
if curr_record.is_some() && curr_record != Some(swap) {
333
+
return Err(anyhow::anyhow!(
334
+
"Bad record swap: current={:?}, expected={}",
335
+
curr_record,
336
+
swap
337
+
));
338
+
}
339
+
}
340
+
}
341
+
}
342
+
}
343
+
}
344
+
345
+
// Load repository
346
+
let repo = Repo::load(&self.storage, curr_root.cid).await?;
347
+
let prev_data = repo.commit.data.clone();
348
+
349
+
// Convert writes to repo operations
350
+
let write_ops = writes
351
+
.iter()
352
+
.map(|w| write_to_op(w))
353
+
.collect::<Result<Vec<_>>>()?;
354
+
355
+
// Format the commit with the repository
356
+
let mut commit = repo.format_commit(write_ops, signing_key).await?;
357
+
358
+
// Find blocks that would be deleted but are referenced by another record
359
+
let dupe_record_cids = self
360
+
.get_duplicate_record_cids(&commit.removed_cids.to_list(), &del_and_update_uris)
361
+
.await?;
362
+
363
+
// Remove duplicates from removed_cids
364
+
for cid in &dupe_record_cids {
365
+
commit.removed_cids.delete(*cid);
366
+
}
367
+
368
+
// Find blocks that are relevant to ops but not included in diff
369
+
let new_record_blocks = commit.relevant_blocks.get_many(&new_record_cids)?;
370
+
if !new_record_blocks.missing.is_empty() {
371
+
let missing_blocks = self.storage.get_blocks(&new_record_blocks.missing).await?;
372
+
commit.relevant_blocks.add_map(missing_blocks.blocks)?;
373
+
}
374
+
375
+
Ok(CommitDataWithOps {
376
+
commit_data: commit,
377
+
ops: commit_ops,
378
+
prev_data: Some(prev_data),
379
+
})
380
+
}
381
+
382
+
/// Index writes to the database
383
+
pub(crate) async fn index_writes(&self, writes: &[PreparedWrite], rev: &str) -> Result<()> {
384
+
let timestamp = chrono::Utc::now().to_rfc3339();
385
+
386
+
for write in writes {
387
+
match write {
388
+
PreparedWrite::Create(w) => {
389
+
let uri = AtUri::from_str(&w.uri)?;
390
+
self.record_transactor
391
+
.index_record(
392
+
uri,
393
+
w.cid,
394
+
Some(&w.record),
395
+
WriteOpAction::Create,
396
+
rev,
397
+
Some(timestamp.clone()),
398
+
)
399
+
.await?;
400
+
}
401
+
PreparedWrite::Update(w) => {
402
+
let uri = AtUri::from_str(&w.uri)?;
403
+
self.record_transactor
404
+
.index_record(
405
+
uri,
406
+
w.cid,
407
+
Some(&w.record),
408
+
WriteOpAction::Update,
409
+
rev,
410
+
Some(timestamp.clone()),
411
+
)
412
+
.await?;
413
+
}
414
+
PreparedWrite::Delete(w) => {
415
+
let uri = AtUri::from_str(&w.uri)?;
416
+
self.record_transactor.delete_record(&uri).await?;
417
+
}
418
+
}
419
+
}
420
+
421
+
Ok(())
422
+
}
423
+
424
+
/// Get record CIDs that are duplicated elsewhere in the repository
425
+
pub(crate) async fn get_duplicate_record_cids(
426
+
&self,
427
+
cids: &[Cid],
428
+
touched_uris: &[AtUri],
429
+
) -> Result<Vec<Cid>> {
430
+
if touched_uris.is_empty() || cids.is_empty() {
431
+
return Ok(Vec::new());
432
+
}
433
+
434
+
// Convert URIs to strings for the query
435
+
let uri_strings: Vec<String> = touched_uris.iter().map(|u| u.to_string()).collect();
436
+
437
+
// Convert CIDs to strings for the query
438
+
let cid_strings: Vec<String> = cids.iter().map(|c| c.to_string()).collect();
439
+
440
+
let did = self.did.clone();
441
+
442
+
// Query for records with these CIDs that aren't in the touched URIs
443
+
let duplicate_cids = self
444
+
.storage
445
+
.db
446
+
.run(move |conn| {
447
+
use rsky_pds::schema::pds::record::dsl::*;
448
+
449
+
record
450
+
.filter(did.eq(&did))
451
+
.filter(cid.eq_any(&cid_strings))
452
+
.filter(uri.ne_all(&uri_strings))
453
+
.select(cid)
454
+
.load::<String>(conn)
455
+
})
456
+
.await?;
457
+
458
+
// Convert strings back to CIDs
459
+
let cids = duplicate_cids
460
+
.into_iter()
461
+
.filter_map(|c| Cid::from_str(&c).ok())
462
+
.collect();
463
+
464
+
Ok(cids)
465
+
}
466
+
}
-11
src/actor_store/repo/mod.rs
-11
src/actor_store/repo/mod.rs
···
1
-
//! Repository operations for actor store.
2
-
3
-
mod reader;
4
-
mod sql_repo_reader;
5
-
mod sql_repo_transactor;
6
-
mod transactor;
7
-
8
-
pub(crate) use reader::RepoReader;
9
-
pub(crate) use sql_repo_reader::SqlRepoReader;
10
-
pub(crate) use sql_repo_transactor::SqlRepoTransactor;
11
-
pub(crate) use transactor::RepoTransactor;
-56
src/actor_store/repo/reader.rs
-56
src/actor_store/repo/reader.rs
···
1
-
//! Repository reader for the actor store.
2
-
3
-
use anyhow::Result;
4
-
use atrium_repo::Cid;
5
-
use rsky_repo::storage::readable_blockstore::ReadableBlockstore as _;
6
-
7
-
use super::sql_repo_reader::SqlRepoReader;
8
-
use crate::{
9
-
actor_store::{ActorDb, blob::BlobReader, record::RecordReader},
10
-
repo::{block_map::BlockMap, types::BlobStore},
11
-
};
12
-
13
-
/// Reader for repository data in the actor store.
14
-
pub(crate) struct RepoReader {
15
-
blob: BlobReader,
16
-
record: RecordReader,
17
-
/// The SQL repository reader.
18
-
storage: SqlRepoReader,
19
-
}
20
-
21
-
impl RepoReader {
22
-
/// Create a new repository reader.
23
-
pub(crate) fn new(db: ActorDb, blobstore: BlobStore) -> Self {
24
-
let blob = BlobReader::new(db.clone(), blobstore);
25
-
let record = RecordReader::new(db.clone());
26
-
let storage = SqlRepoReader::new(db);
27
-
28
-
Self {
29
-
blob,
30
-
record,
31
-
storage,
32
-
}
33
-
}
34
-
35
-
/// Get event data for synchronization.
36
-
pub(crate) async fn get_sync_event_data(&self) -> Result<SyncEventData> {
37
-
let root = self.storage.get_root_detailed().await?;
38
-
let blocks = self.storage.get_blocks(vec![root.cid]).await?;
39
-
40
-
Ok(SyncEventData {
41
-
cid: root.cid,
42
-
rev: root.rev,
43
-
blocks: blocks.blocks,
44
-
})
45
-
}
46
-
}
47
-
48
-
/// Data for sync events.
49
-
pub(crate) struct SyncEventData {
50
-
/// The CID of the repository root.
51
-
pub cid: Cid,
52
-
/// The revision of the repository.
53
-
pub rev: String,
54
-
/// The blocks in the repository.
55
-
pub blocks: BlockMap,
56
-
}
-188
src/actor_store/repo/sql_repo_reader.rs
-188
src/actor_store/repo/sql_repo_reader.rs
···
1
-
//! SQL-based repository reader.
2
-
3
-
use anyhow::{Context as _, Result};
4
-
use atrium_repo::{
5
-
Cid,
6
-
blockstore::{AsyncBlockStoreRead, Error as BlockstoreError},
7
-
};
8
-
use rsky_repo::storage::readable_blockstore::ReadableBlockstore;
9
-
use sqlx::Row;
10
-
use std::str::FromStr;
11
-
use std::sync::Arc;
12
-
use tokio::sync::RwLock;
13
-
14
-
use crate::{
15
-
actor_store::ActorDb,
16
-
repo::block_map::{BlockMap, BlocksAndMissing, CidSet},
17
-
};
18
-
19
-
/// SQL-based repository reader.
20
-
pub(crate) struct SqlRepoReader {
21
-
/// Cache for blocks to avoid redundant database queries.
22
-
pub cache: Arc<RwLock<BlockMap>>,
23
-
/// Database connection.
24
-
pub db: ActorDb,
25
-
}
26
-
27
-
/// Repository root with CID and revision.
28
-
pub(crate) struct RootInfo {
29
-
/// CID of the repository root.
30
-
pub cid: Cid,
31
-
/// Revision of the repository.
32
-
pub rev: String,
33
-
}
34
-
35
-
impl SqlRepoReader {
36
-
/// Create a new SQL repository reader.
37
-
pub(crate) fn new(db: ActorDb) -> Self {
38
-
Self {
39
-
cache: Arc::new(RwLock::new(BlockMap::new())),
40
-
db,
41
-
}
42
-
}
43
-
// async getRoot(): Promise<CID> {
44
-
// async getCarStream(since?: string) {
45
-
// async *iterateCarBlocks(since?: string): AsyncIterable<CarBlock> {
46
-
// async getBlockRange(since?: string, cursor?: RevCursor) {
47
-
// async countBlocks(): Promise<number> {
48
-
// async destroy(): Promise<void> {
49
-
50
-
/// Get the detailed root information.
51
-
pub(crate) async fn get_root_detailed(&self) -> Result<RootInfo> {
52
-
let row = sqlx::query!(r#"SELECT cid, rev FROM repo_root"#)
53
-
.fetch_one(&self.db.pool)
54
-
.await
55
-
.context("failed to fetch repo root")?;
56
-
57
-
Ok(RootInfo {
58
-
cid: Cid::from_str(&row.cid)?,
59
-
rev: row.rev,
60
-
})
61
-
}
62
-
}
63
-
64
-
impl ReadableBlockstore for SqlRepoReader {
65
-
async fn get_bytes(&self, cid: &Cid) -> Result<Option<Vec<u8>>> {
66
-
// First check the cache
67
-
{
68
-
let cache_guard = self.cache.read().await;
69
-
if let Some(cached) = cache_guard.get(*cid) {
70
-
return Ok(Some(cached.clone()));
71
-
}
72
-
}
73
-
74
-
// Not in cache, query from database
75
-
let cid_str = cid.to_string();
76
-
77
-
let content = sqlx::query!(r#"SELECT content FROM repo_block WHERE cid = ?"#, cid_str,)
78
-
.fetch_optional(&self.db.pool)
79
-
.await
80
-
.context("failed to fetch block content")?
81
-
.map(|row| row.content);
82
-
83
-
// If found, update the cache
84
-
if let Some(bytes) = &content {
85
-
let mut cache_guard = self.cache.write().await;
86
-
cache_guard.set(*cid, bytes.clone());
87
-
}
88
-
89
-
Ok(content)
90
-
}
91
-
92
-
async fn has(&self, cid: &Cid) -> Result<bool> {
93
-
self.get_bytes(cid).await.map(|bytes| bytes.is_some())
94
-
}
95
-
96
-
/// Get blocks from the database.
97
-
async fn get_blocks(&self, cids: Vec<Cid>) -> Result<BlocksAndMissing> {
98
-
let cached = { self.cache.write().await.get_many(cids)? }; // TODO: use read lock?
99
-
100
-
if cached.missing.is_empty() {
101
-
return Ok(cached);
102
-
}
103
-
104
-
let missing = cached.missing.clone();
105
-
let missing_strings: Vec<String> = missing.iter().map(|c| c.to_string()).collect();
106
-
107
-
let mut blocks = BlockMap::new();
108
-
let mut missing_set = CidSet::new(None);
109
-
for cid in &missing {
110
-
missing_set.add(*cid);
111
-
}
112
-
113
-
// Process in chunks to avoid too many parameters
114
-
for chunk in missing_strings.chunks(500) {
115
-
let placeholders = std::iter::repeat("?")
116
-
.take(chunk.len())
117
-
.collect::<Vec<_>>()
118
-
.join(",");
119
-
120
-
let query = format!(
121
-
"SELECT cid, content FROM repo_block
122
-
WHERE cid IN ({})
123
-
ORDER BY cid",
124
-
placeholders
125
-
);
126
-
127
-
let mut query_builder = sqlx::query(&query);
128
-
for cid in chunk {
129
-
query_builder = query_builder.bind(cid);
130
-
}
131
-
132
-
let rows = query_builder
133
-
.map(|row: sqlx::sqlite::SqliteRow| {
134
-
(
135
-
row.get::<String, _>("cid"),
136
-
row.get::<Vec<u8>, _>("content"),
137
-
)
138
-
})
139
-
.fetch_all(&self.db.pool)
140
-
.await?;
141
-
142
-
for (cid_str, content) in rows {
143
-
let cid = Cid::from_str(&cid_str)?;
144
-
blocks.set(cid, content);
145
-
missing_set.delete(cid);
146
-
}
147
-
}
148
-
149
-
// Update cache
150
-
self.cache.write().await.add_map(blocks.clone())?; // TODO: unnecessary clone?
151
-
152
-
// Add cached blocks
153
-
blocks.add_map(cached.blocks)?;
154
-
155
-
Ok(BlocksAndMissing {
156
-
blocks,
157
-
missing: missing_set.to_list(),
158
-
})
159
-
}
160
-
}
161
-
162
-
impl AsyncBlockStoreRead for SqlRepoReader {
163
-
async fn read_block(&mut self, cid: Cid) -> Result<Vec<u8>, BlockstoreError> {
164
-
let bytes = self
165
-
.get_bytes(&cid)
166
-
.await
167
-
.unwrap()
168
-
.ok_or(BlockstoreError::CidNotFound)?;
169
-
Ok(bytes)
170
-
}
171
-
172
-
fn read_block_into(
173
-
&mut self,
174
-
cid: Cid,
175
-
contents: &mut Vec<u8>,
176
-
) -> impl Future<Output = Result<(), BlockstoreError>> + Send {
177
-
async move {
178
-
let bytes = self
179
-
.get_bytes(&cid)
180
-
.await
181
-
.unwrap()
182
-
.ok_or(BlockstoreError::CidNotFound)?;
183
-
contents.clear();
184
-
contents.extend_from_slice(&bytes);
185
-
Ok(())
186
-
}
187
-
}
188
-
}
-272
src/actor_store/repo/sql_repo_transactor.rs
-272
src/actor_store/repo/sql_repo_transactor.rs
···
1
-
//! SQL-based repository transactor.
2
-
3
-
use std::str::FromStr;
4
-
5
-
use anyhow::Result;
6
-
use atrium_repo::{
7
-
Cid,
8
-
blockstore::{AsyncBlockStoreWrite, Error as BlockstoreError},
9
-
};
10
-
use sha2::Digest;
11
-
12
-
use crate::{
13
-
actor_store::ActorDb,
14
-
repo::{block_map::BlockMap, types::CommitData},
15
-
};
16
-
17
-
use super::sql_repo_reader::{RootInfo, SqlRepoReader};
18
-
19
-
/// SQL-based repository transactor that extends the reader.
20
-
pub(crate) struct SqlRepoTransactor {
21
-
/// The inner reader.
22
-
pub reader: SqlRepoReader,
23
-
/// Cache for blocks.
24
-
pub cache: BlockMap,
25
-
/// Current timestamp.
26
-
pub now: Option<String>,
27
-
}
28
-
29
-
impl SqlRepoTransactor {
30
-
/// Create a new SQL repository transactor.
31
-
pub(crate) fn new(db: ActorDb, did: String, now: Option<String>) -> Self {
32
-
Self {
33
-
reader: SqlRepoReader::new(db, did),
34
-
cache: BlockMap::new(),
35
-
now,
36
-
}
37
-
}
38
-
39
-
/// Get the root CID and revision of the repository.
40
-
pub(crate) async fn get_root_detailed(&self) -> Result<RootInfo> {
41
-
let row = sqlx::query!(
42
-
r#"
43
-
SELECT cid, rev
44
-
FROM repo_root
45
-
WHERE did = ?
46
-
LIMIT 1
47
-
"#,
48
-
self.reader.did
49
-
)
50
-
.fetch_one(&self.reader.db.pool)
51
-
.await?;
52
-
53
-
let cid = Cid::from_str(&row.cid)?;
54
-
Ok(RootInfo { cid, rev: row.rev })
55
-
}
56
-
57
-
/// Proactively cache all blocks from a particular commit.
58
-
pub(crate) async fn cache_rev(&mut self, rev: &str) -> Result<()> {
59
-
let rows = sqlx::query!(
60
-
r#"
61
-
SELECT cid, content
62
-
FROM repo_block
63
-
WHERE repoRev = ?
64
-
LIMIT 15
65
-
"#,
66
-
rev
67
-
)
68
-
.fetch_all(&self.reader.db.pool)
69
-
.await?;
70
-
71
-
for row in rows {
72
-
let cid = Cid::from_str(&row.cid)?;
73
-
self.cache.set(cid, row.content.clone());
74
-
}
75
-
Ok(())
76
-
}
77
-
78
-
/// Apply a commit to the repository.
79
-
pub(crate) async fn apply_commit(&self, commit: &CommitData, is_create: bool) -> Result<()> {
80
-
let is_create = is_create || false;
81
-
let removed_cids_list = commit.removed_cids.to_list();
82
-
83
-
// Run these operations in parallel for better performance
84
-
tokio::try_join!(
85
-
self.update_root(commit.cid, &commit.rev, is_create),
86
-
self.put_many(&commit.new_blocks, &commit.rev),
87
-
self.delete_many(&removed_cids_list)
88
-
)?;
89
-
90
-
Ok(())
91
-
}
92
-
93
-
/// Update the repository root.
94
-
pub(crate) async fn update_root(&self, cid: Cid, rev: &str, is_create: bool) -> Result<()> {
95
-
let cid_str = cid.to_string();
96
-
let did = self.reader.did.clone();
97
-
let now = self.now.clone();
98
-
99
-
if is_create {
100
-
sqlx::query!(
101
-
r#"
102
-
INSERT INTO repo_root (did, cid, rev, indexedAt)
103
-
VALUES (?, ?, ?, ?)
104
-
"#,
105
-
did,
106
-
cid_str,
107
-
rev,
108
-
now
109
-
)
110
-
.execute(&self.reader.db.pool)
111
-
.await?;
112
-
} else {
113
-
sqlx::query!(
114
-
r#"
115
-
UPDATE repo_root
116
-
SET cid = ?, rev = ?, indexedAt = ?
117
-
WHERE did = ?
118
-
"#,
119
-
cid_str,
120
-
rev,
121
-
now,
122
-
did
123
-
)
124
-
.execute(&self.reader.db.pool)
125
-
.await?;
126
-
}
127
-
128
-
Ok(())
129
-
}
130
-
131
-
/// Put a block into the repository.
132
-
pub(crate) async fn put_block(&self, cid: Cid, block: &[u8], rev: &str) -> Result<()> {
133
-
let cid_str = cid.to_string();
134
-
135
-
let block_len = block.len() as i64;
136
-
sqlx::query!(
137
-
r#"
138
-
INSERT INTO repo_block (cid, repoRev, size, content)
139
-
VALUES (?, ?, ?, ?)
140
-
ON CONFLICT DO NOTHING
141
-
"#,
142
-
cid_str,
143
-
rev,
144
-
block_len,
145
-
block
146
-
)
147
-
.execute(&self.reader.db.pool)
148
-
.await?;
149
-
150
-
Ok(())
151
-
}
152
-
153
-
/// Put many blocks into the repository.
154
-
pub(crate) async fn put_many(&self, blocks: &BlockMap, rev: &str) -> Result<()> {
155
-
if blocks.size() == 0 {
156
-
return Ok(());
157
-
}
158
-
159
-
let did = self.reader.did.clone();
160
-
let mut batch = Vec::new();
161
-
162
-
blocks.to_owned().map.into_iter().for_each(|(cid, bytes)| {
163
-
batch.push((cid, did.clone(), rev, bytes.0.len() as i64, bytes.0));
164
-
});
165
-
166
-
// Process in chunks to avoid too many parameters
167
-
for chunk in batch.chunks(50) {
168
-
let placeholders = chunk
169
-
.iter()
170
-
.map(|_| "(?, ?, ?, ?, ?)")
171
-
.collect::<Vec<_>>()
172
-
.join(", ");
173
-
174
-
let query = format!(
175
-
"INSERT INTO repo_block (cid, did, repoRev, size, content) VALUES {} ON CONFLICT DO NOTHING",
176
-
placeholders
177
-
);
178
-
179
-
let mut query_builder = sqlx::query(&query);
180
-
for (cid, did, rev, size, content) in chunk {
181
-
query_builder = query_builder
182
-
.bind(cid)
183
-
.bind(did)
184
-
.bind(rev)
185
-
.bind(size)
186
-
.bind(content);
187
-
}
188
-
189
-
query_builder.execute(&self.reader.db.pool).await?;
190
-
}
191
-
192
-
Ok(())
193
-
}
194
-
195
-
/// Delete many blocks from the repository.
196
-
pub(crate) async fn delete_many(&self, cids: &[Cid]) -> Result<()> {
197
-
if cids.is_empty() {
198
-
return Ok(());
199
-
}
200
-
201
-
let did = self.reader.did.clone();
202
-
let cid_strings: Vec<String> = cids.iter().map(|c| c.to_string()).collect();
203
-
204
-
// Process in chunks to avoid too many parameters
205
-
for chunk in cid_strings.chunks(500) {
206
-
let placeholders = std::iter::repeat("?")
207
-
.take(chunk.len())
208
-
.collect::<Vec<_>>()
209
-
.join(",");
210
-
211
-
let query = format!(
212
-
"DELETE FROM repo_block WHERE did = ? AND cid IN ({})",
213
-
placeholders
214
-
);
215
-
216
-
let mut query_builder = sqlx::query(&query);
217
-
query_builder = query_builder.bind(&did);
218
-
for cid in chunk {
219
-
query_builder = query_builder.bind(cid);
220
-
}
221
-
222
-
query_builder.execute(&self.reader.db.pool).await?;
223
-
}
224
-
225
-
Ok(())
226
-
}
227
-
}
228
-
229
-
#[async_trait::async_trait]
230
-
impl AsyncBlockStoreWrite for SqlRepoTransactor {
231
-
fn write_block(
232
-
&mut self,
233
-
codec: u64,
234
-
hash: u64,
235
-
contents: &[u8],
236
-
) -> impl Future<Output = Result<Cid, BlockstoreError>> + Send {
237
-
let contents = contents.to_vec();
238
-
let rev = self.now.clone();
239
-
240
-
async move {
241
-
let digest = match hash {
242
-
atrium_repo::blockstore::SHA2_256 => sha2::Sha256::digest(&contents),
243
-
_ => return Err(BlockstoreError::UnsupportedHash(hash)),
244
-
};
245
-
246
-
let multihash = atrium_repo::Multihash::wrap(hash, &digest)
247
-
.map_err(|_| BlockstoreError::UnsupportedHash(hash))?;
248
-
249
-
let cid = Cid::new_v1(codec, multihash);
250
-
let cid_str = cid.to_string();
251
-
let contents_len = contents.len() as i64;
252
-
253
-
sqlx::query!(
254
-
r#"
255
-
INSERT INTO repo_block (cid, repoRev, size, content)
256
-
VALUES (?, ?, ?, ?)
257
-
ON CONFLICT DO NOTHING
258
-
"#,
259
-
cid_str,
260
-
rev,
261
-
contents_len,
262
-
contents
263
-
)
264
-
.execute(&self.reader.db.pool)
265
-
.await
266
-
.map_err(|e| BlockstoreError::Other(Box::new(e)))?;
267
-
268
-
self.cache.set(cid, contents);
269
-
Ok(cid)
270
-
}
271
-
}
272
-
}
-409
src/actor_store/repo/transactor.rs
-409
src/actor_store/repo/transactor.rs
···
1
-
//! Repository transactor for the actor store.
2
-
3
-
use anyhow::{Context as _, Result};
4
-
use atrium_repo::Cid;
5
-
use rsky_syntax::aturi::AtUri;
6
-
use std::sync::Arc;
7
-
8
-
use crate::{
9
-
SigningKey,
10
-
actor_store::{
11
-
ActorDb,
12
-
blob::{BackgroundQueue, BlobTransactor},
13
-
record::RecordTransactor,
14
-
repo::{reader::RepoReader, sql_repo_transactor::SqlRepoTransactor},
15
-
resources::ActorStoreResources,
16
-
},
17
-
repo::{
18
-
Repo,
19
-
block_map::BlockMap,
20
-
types::{
21
-
BlobStore, CommitAction, CommitDataWithOps, CommitOp, PreparedCreate, PreparedWrite,
22
-
WriteOpAction, format_data_key,
23
-
},
24
-
},
25
-
};
26
-
27
-
/// Repository transactor for the actor store.
28
-
pub(crate) struct RepoTransactor {
29
-
/// The inner reader.
30
-
pub reader: RepoReader,
31
-
/// BlobTransactor for handling blobs.
32
-
pub blob: BlobTransactor,
33
-
/// RecordTransactor for handling records.
34
-
pub record: RecordTransactor,
35
-
/// SQL repository transactor.
36
-
pub storage: SqlRepoTransactor,
37
-
}
38
-
39
-
impl RepoTransactor {
40
-
/// Create a new repository transactor.
41
-
pub(crate) fn new(
42
-
db: ActorDb,
43
-
blobstore: BlobStore,
44
-
did: String,
45
-
signing_key: Arc<SigningKey>,
46
-
background_queue: BackgroundQueue,
47
-
now: Option<String>,
48
-
) -> Self {
49
-
// Create a new RepoReader
50
-
let reader = RepoReader::new(db.clone(), blobstore.clone(), did.clone(), signing_key);
51
-
52
-
// Create a new BlobTransactor
53
-
let blob = BlobTransactor::new(db.clone(), blobstore.clone(), background_queue);
54
-
55
-
// Create a new RecordTransactor
56
-
let record = RecordTransactor::new(db.clone(), blobstore);
57
-
58
-
// Create a new SQL repository transactor
59
-
let storage = SqlRepoTransactor::new(db, did.clone(), now);
60
-
61
-
Self {
62
-
reader,
63
-
blob,
64
-
record,
65
-
storage,
66
-
}
67
-
}
68
-
69
-
/// Try to load a repository.
70
-
pub(crate) async fn maybe_load_repo(&self) -> Result<Option<Repo>> {
71
-
// Query the repository root
72
-
let root = sqlx::query!("SELECT cid FROM repo_root LIMIT 1")
73
-
.fetch_optional(&self.db.pool)
74
-
.await
75
-
.context("failed to query repo root")?;
76
-
77
-
// If found, load the repo
78
-
if let Some(row) = root {
79
-
let cid = Cid::try_from(&row.cid)?;
80
-
let repo = Repo::load(&self.storage, cid).await?;
81
-
Ok(Some(repo))
82
-
} else {
83
-
Ok(None)
84
-
}
85
-
}
86
-
87
-
/// Create a new repository with the given writes.
88
-
pub(crate) async fn create_repo(
89
-
&self,
90
-
writes: Vec<PreparedCreate>,
91
-
) -> Result<CommitDataWithOps> {
92
-
// Assert we're in a transaction
93
-
self.db.assert_transaction()?;
94
-
95
-
// Convert writes to operations
96
-
let ops = writes.iter().map(|w| create_write_to_op(w)).collect();
97
-
98
-
// Format the initial commit
99
-
let commit =
100
-
Repo::format_init_commit(&self.storage, &self.did, &self.signing_key, ops).await?;
101
-
102
-
// Apply the commit, index the writes, and process blobs in parallel
103
-
let results = futures::future::join3(
104
-
self.storage.apply_commit(&commit, true),
105
-
self.index_writes(&writes, &commit.rev),
106
-
self.blob.process_write_blobs(&commit.rev, &writes),
107
-
)
108
-
.await;
109
-
110
-
// Check for errors in each parallel task
111
-
results.0?;
112
-
results.1?;
113
-
results.2?;
114
-
115
-
// Create commit operations
116
-
let ops = writes
117
-
.iter()
118
-
.map(|w| CommitOp {
119
-
action: CommitAction::Create,
120
-
path: format_data_key(&w.uri.collection, &w.uri.rkey),
121
-
cid: Some(w.cid),
122
-
prev: None,
123
-
})
124
-
.collect();
125
-
126
-
// Return the commit data with operations
127
-
Ok(CommitDataWithOps {
128
-
commit_data: commit,
129
-
ops,
130
-
prev_data: None,
131
-
})
132
-
}
133
-
134
-
/// Process writes to the repository.
135
-
pub(crate) async fn process_writes(
136
-
&self,
137
-
writes: Vec<PreparedWrite>,
138
-
swap_commit_cid: Option<Cid>,
139
-
) -> Result<CommitDataWithOps> {
140
-
// Assert we're in a transaction
141
-
self.db.assert_transaction()?;
142
-
143
-
// Check write limit
144
-
if writes.len() > 200 {
145
-
return Err(anyhow::anyhow!("Too many writes. Max: 200"));
146
-
}
147
-
148
-
// Format the commit
149
-
let commit = self.format_commit(writes, swap_commit_cid).await?;
150
-
151
-
// Check commit size limit (2MB)
152
-
if commit.commit_data.relevant_blocks.byte_size()? > 2_000_000 {
153
-
return Err(anyhow::anyhow!("Too many writes. Max event size: 2MB"));
154
-
}
155
-
156
-
// Apply the commit, index the writes, and process blobs in parallel
157
-
let results = futures::future::join3(
158
-
self.storage.apply_commit(&commit.commit_data, false),
159
-
self.index_writes(writes, &commit.commit_data.rev),
160
-
self.blob
161
-
.process_write_blobs(&commit.commit_data.rev, writes),
162
-
)
163
-
.await;
164
-
165
-
// Check for errors in each parallel task
166
-
results.0?;
167
-
results.1?;
168
-
results.2?;
169
-
170
-
Ok(commit)
171
-
}
172
-
173
-
/// Format a commit for the given writes.
174
-
pub(crate) async fn format_commit(
175
-
&self,
176
-
writes: Vec<PreparedWrite>,
177
-
swap_commit: Option<Cid>,
178
-
) -> Result<CommitDataWithOps> {
179
-
// Get the current root
180
-
let curr_root = self.storage.get_root_detailed().await?;
181
-
if curr_root.is_none() {
182
-
return Err(anyhow::anyhow!("No repo root found for {}", self.did));
183
-
}
184
-
185
-
let curr_root = curr_root.unwrap();
186
-
187
-
// Check commit swap if requested
188
-
if let Some(swap) = swap_commit {
189
-
if curr_root.cid != swap {
190
-
return Err(anyhow::anyhow!(
191
-
"Bad commit swap: current={}, expected={}",
192
-
curr_root.cid,
193
-
swap
194
-
));
195
-
}
196
-
}
197
-
198
-
// Cache the revision for better performance
199
-
self.storage.cache_rev(&curr_root.rev).await?;
200
-
201
-
// Prepare collections for tracking changes
202
-
let mut new_record_cids = Vec::new();
203
-
let mut del_and_update_uris = Vec::new();
204
-
let mut commit_ops = Vec::new();
205
-
206
-
// Process each write
207
-
for write in writes {
208
-
let action = &write.action;
209
-
let uri = &write.uri;
210
-
let swap_cid = write.swap_cid;
211
-
212
-
// Track new record CIDs
213
-
if *action != WriteOpAction::Delete {
214
-
new_record_cids.push(write.cid);
215
-
}
216
-
217
-
// Track deleted/updated URIs
218
-
if *action != WriteOpAction::Create {
219
-
del_and_update_uris.push(uri.clone());
220
-
}
221
-
222
-
// Get the current record if it exists
223
-
let record = self.record.get_record(uri, None, true).await?;
224
-
let curr_record = record.as_ref().map(|r| Cid::try_from(&r.cid).unwrap());
225
-
226
-
// Create commit operation
227
-
let mut op = CommitOp {
228
-
action: action.clone(),
229
-
path: format_data_key(&uri.collection, &uri.rkey),
230
-
cid: if *action == WriteOpAction::Delete {
231
-
None
232
-
} else {
233
-
Some(write.cid)
234
-
},
235
-
prev: curr_record,
236
-
};
237
-
238
-
commit_ops.push(op);
239
-
240
-
// Validate swap_cid conditions
241
-
if swap_cid.is_some() {
242
-
match action {
243
-
WriteOpAction::Create if swap_cid != Some(None) => {
244
-
return Err(anyhow::anyhow!(
245
-
"Bad record swap: there should be no current record for a create"
246
-
));
247
-
}
248
-
WriteOpAction::Update if swap_cid == Some(None) => {
249
-
return Err(anyhow::anyhow!(
250
-
"Bad record swap: there should be a current record for an update"
251
-
));
252
-
}
253
-
WriteOpAction::Delete if swap_cid == Some(None) => {
254
-
return Err(anyhow::anyhow!(
255
-
"Bad record swap: there should be a current record for a delete"
256
-
));
257
-
}
258
-
_ => {}
259
-
}
260
-
261
-
if let Some(Some(swap)) = swap_cid {
262
-
if curr_record.is_some() && curr_record != Some(swap) {
263
-
return Err(anyhow::anyhow!(
264
-
"Bad record swap: current={:?}, expected={}",
265
-
curr_record,
266
-
swap
267
-
));
268
-
}
269
-
}
270
-
}
271
-
}
272
-
273
-
// Load the repo
274
-
let repo = Repo::load(&self.storage, curr_root.cid).await?;
275
-
let prev_data = repo.commit.data.clone();
276
-
277
-
// Convert writes to ops
278
-
let write_ops = writes.iter().map(|w| write_to_op(w)).collect();
279
-
280
-
// Format the commit
281
-
let commit = repo.format_commit(write_ops, &self.signing_key).await?;
282
-
283
-
// Find blocks that would be deleted but are referenced by another record
284
-
let dupe_record_cids = self
285
-
.get_duplicate_record_cids(&commit.removed_cids.to_list(), &del_and_update_uris)
286
-
.await?;
287
-
288
-
// Remove duplicates from removed_cids
289
-
for cid in &dupe_record_cids {
290
-
commit.removed_cids.delete(*cid);
291
-
}
292
-
293
-
// Find blocks that are relevant to ops but not included in diff
294
-
let new_record_blocks = commit.relevant_blocks.get_many(&new_record_cids)?;
295
-
if !new_record_blocks.missing.is_empty() {
296
-
let missing_blocks = self
297
-
.storage
298
-
.reader
299
-
.get_blocks(&new_record_blocks.missing)
300
-
.await?;
301
-
commit.relevant_blocks.add_map(missing_blocks.blocks)?;
302
-
}
303
-
304
-
Ok(CommitDataWithOps {
305
-
commit_data: commit,
306
-
ops: commit_ops,
307
-
prev_data: Some(prev_data),
308
-
})
309
-
}
310
-
311
-
/// Index writes to the database.
312
-
pub(crate) async fn index_writes(&self, writes: &[PreparedWrite], rev: &str) -> Result<()> {
313
-
// Assert we're in a transaction
314
-
self.db.assert_transaction()?;
315
-
316
-
// Process each write in parallel
317
-
let futures = writes.iter().map(|write| async move {
318
-
match write.action {
319
-
WriteOpAction::Create | WriteOpAction::Update => {
320
-
self.record
321
-
.index_record(
322
-
&write.uri,
323
-
&write.cid,
324
-
&write.record,
325
-
&write.action,
326
-
rev,
327
-
&self.now,
328
-
)
329
-
.await
330
-
}
331
-
WriteOpAction::Delete => self.record.delete_record(&write.uri).await,
332
-
}
333
-
});
334
-
335
-
// Wait for all indexing operations to complete
336
-
futures::future::try_join_all(futures).await?;
337
-
338
-
Ok(())
339
-
}
340
-
341
-
/// Get record CIDs that are duplicated elsewhere in the repository.
342
-
pub(crate) async fn get_duplicate_record_cids(
343
-
&self,
344
-
cids: &[Cid],
345
-
touched_uris: &[AtUri],
346
-
) -> Result<Vec<Cid>> {
347
-
if touched_uris.is_empty() || cids.is_empty() {
348
-
return Ok(Vec::new());
349
-
}
350
-
351
-
// Convert CIDs and URIs to strings
352
-
let cid_strs: Vec<String> = cids.iter().map(|c| c.to_string()).collect();
353
-
let uri_strs: Vec<String> = touched_uris.iter().map(|u| u.to_string()).collect();
354
-
355
-
// Query the database for duplicates
356
-
let rows = sqlx::query!(
357
-
"SELECT cid FROM record WHERE cid IN (?) AND uri NOT IN (?)",
358
-
cid_strs.join(","),
359
-
uri_strs.join(",")
360
-
)
361
-
.fetch_all(&self.db.pool)
362
-
.await
363
-
.context("failed to query duplicate record CIDs")?;
364
-
365
-
// Convert to CIDs
366
-
let result = rows
367
-
.into_iter()
368
-
.map(|row| Cid::try_from(&row.cid))
369
-
.collect::<Result<Vec<_>, _>>()?;
370
-
371
-
Ok(result)
372
-
}
373
-
}
374
-
375
-
// Helper functions
376
-
377
-
/// Convert a PreparedCreate to an operation.
378
-
fn create_write_to_op(write: &PreparedCreate) -> WriteOp {
379
-
WriteOp {
380
-
action: WriteOpAction::Create,
381
-
collection: write.uri.collection.clone(),
382
-
rkey: write.uri.rkey.clone(),
383
-
record: write.record.clone(),
384
-
}
385
-
}
386
-
387
-
/// Convert a PreparedWrite to an operation.
388
-
fn write_to_op(write: &PreparedWrite) -> WriteOp {
389
-
match write.action {
390
-
WriteOpAction::Create => WriteOp {
391
-
action: WriteOpAction::Create,
392
-
collection: write.uri.collection.clone(),
393
-
rkey: write.uri.rkey.clone(),
394
-
record: write.record.clone(),
395
-
},
396
-
WriteOpAction::Update => WriteOp {
397
-
action: WriteOpAction::Update,
398
-
collection: write.uri.collection.clone(),
399
-
rkey: write.uri.rkey.clone(),
400
-
record: write.record.clone(),
401
-
},
402
-
WriteOpAction::Delete => WriteOp {
403
-
action: WriteOpAction::Delete,
404
-
collection: write.uri.collection.clone(),
405
-
rkey: write.uri.rkey.clone(),
406
-
record: None,
407
-
},
408
-
}
409
-
}
+574
src/actor_store/sql_repo.rs
+574
src/actor_store/sql_repo.rs
···
1
+
use anyhow::{Context as _, Result};
2
+
use atrium_repo::Cid;
3
+
use atrium_repo::blockstore::{
4
+
AsyncBlockStoreRead, AsyncBlockStoreWrite, Error as BlockstoreError,
5
+
};
6
+
use diesel::prelude::*;
7
+
use diesel::r2d2::{self, ConnectionManager};
8
+
use diesel::sqlite::SqliteConnection;
9
+
use futures::{StreamExt, TryStreamExt, stream};
10
+
use rsky_pds::models::{RepoBlock, RepoRoot};
11
+
use rsky_repo::block_map::{BlockMap, BlocksAndMissing};
12
+
use rsky_repo::car::blocks_to_car_file;
13
+
use rsky_repo::cid_set::CidSet;
14
+
use rsky_repo::storage::CidAndRev;
15
+
use rsky_repo::storage::RepoRootError::RepoRootNotFoundError;
16
+
use rsky_repo::storage::readable_blockstore::ReadableBlockstore;
17
+
use rsky_repo::storage::types::RepoStorage;
18
+
use rsky_repo::types::CommitData;
19
+
use sha2::{Digest, Sha256};
20
+
use std::future::Future;
21
+
use std::pin::Pin;
22
+
use std::str::FromStr;
23
+
use std::sync::Arc;
24
+
use tokio::sync::RwLock;
25
+
26
+
use crate::actor_store::db::ActorDb;
27
+
28
+
#[derive(Clone, Debug)]
29
+
pub struct SqlRepoStorage {
30
+
/// In-memory cache for blocks
31
+
pub cache: Arc<RwLock<BlockMap>>,
32
+
/// Database connection
33
+
pub db: ActorDb,
34
+
/// DID of the actor
35
+
pub did: String,
36
+
/// Current timestamp
37
+
pub now: String,
38
+
}
39
+
40
+
impl SqlRepoStorage {
41
+
/// Create a new SQL repository storage
42
+
pub fn new(did: String, db: ActorDb, now: Option<String>) -> Self {
43
+
let now = now.unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
44
+
45
+
Self {
46
+
cache: Arc::new(RwLock::new(BlockMap::new())),
47
+
db,
48
+
did,
49
+
now,
50
+
}
51
+
}
52
+
53
+
/// Get the CAR stream for the repository
54
+
pub async fn get_car_stream(&self, since: Option<String>) -> Result<Vec<u8>> {
55
+
match self.get_root().await {
56
+
None => Err(anyhow::Error::new(RepoRootNotFoundError)),
57
+
Some(root) => {
58
+
let mut car = BlockMap::new();
59
+
let mut cursor: Option<CidAndRev> = None;
60
+
61
+
loop {
62
+
let blocks = self.get_block_range(&since, &cursor).await?;
63
+
if blocks.is_empty() {
64
+
break;
65
+
}
66
+
67
+
// Add blocks to car
68
+
for block in &blocks {
69
+
car.set(Cid::from_str(&block.cid)?, block.content.clone());
70
+
}
71
+
72
+
if let Some(last_block) = blocks.last() {
73
+
cursor = Some(CidAndRev {
74
+
cid: Cid::from_str(&last_block.cid)?,
75
+
rev: last_block.repoRev.clone(),
76
+
});
77
+
} else {
78
+
break;
79
+
}
80
+
}
81
+
82
+
blocks_to_car_file(Some(&root), car).await
83
+
}
84
+
}
85
+
}
86
+
87
+
/// Get a range of blocks from the database
88
+
pub async fn get_block_range(
89
+
&self,
90
+
since: &Option<String>,
91
+
cursor: &Option<CidAndRev>,
92
+
) -> Result<Vec<RepoBlock>> {
93
+
let did = self.did.clone();
94
+
95
+
self.db
96
+
.run(move |conn| {
97
+
use rsky_pds::schema::pds::repo_block::dsl::*;
98
+
99
+
let mut query = repo_block.filter(did.eq(&did)).limit(500).into_boxed();
100
+
101
+
if let Some(c) = cursor {
102
+
query = query.filter(
103
+
repoRev
104
+
.lt(&c.rev)
105
+
.or(repoRev.eq(&c.rev).and(cid.lt(&c.cid.to_string()))),
106
+
);
107
+
}
108
+
109
+
if let Some(s) = since {
110
+
query = query.filter(repoRev.gt(s));
111
+
}
112
+
113
+
query
114
+
.order((repoRev.desc(), cid.desc()))
115
+
.load::<RepoBlock>(conn)
116
+
})
117
+
.await
118
+
}
119
+
120
+
/// Count total blocks for this repository
121
+
pub async fn count_blocks(&self) -> Result<i64> {
122
+
let did = self.did.clone();
123
+
124
+
self.db
125
+
.run(move |conn| {
126
+
use rsky_pds::schema::pds::repo_block::dsl::*;
127
+
128
+
repo_block.filter(did.eq(&did)).count().get_result(conn)
129
+
})
130
+
.await
131
+
}
132
+
133
+
/// Proactively cache blocks from a specific revision
134
+
pub async fn cache_rev(&mut self, rev: &str) -> Result<()> {
135
+
let did = self.did.clone();
136
+
let rev_string = rev.to_string();
137
+
138
+
let blocks = self
139
+
.db
140
+
.run(move |conn| {
141
+
use rsky_pds::schema::pds::repo_block::dsl::*;
142
+
143
+
repo_block
144
+
.filter(did.eq(&did))
145
+
.filter(repoRev.eq(&rev_string))
146
+
.select((cid, content))
147
+
.limit(15)
148
+
.load::<(String, Vec<u8>)>(conn)
149
+
})
150
+
.await?;
151
+
152
+
let mut cache_guard = self.cache.write().await;
153
+
for (cid_str, content) in blocks {
154
+
let cid = Cid::from_str(&cid_str)?;
155
+
cache_guard.set(cid, content);
156
+
}
157
+
158
+
Ok(())
159
+
}
160
+
161
+
/// Delete multiple blocks by their CIDs
162
+
pub async fn delete_many(&self, cids: Vec<Cid>) -> Result<()> {
163
+
if cids.is_empty() {
164
+
return Ok(());
165
+
}
166
+
167
+
let did = self.did.clone();
168
+
let cid_strings: Vec<String> = cids.into_iter().map(|c| c.to_string()).collect();
169
+
170
+
// Process in chunks to avoid too many parameters
171
+
for chunk in cid_strings.chunks(100) {
172
+
let chunk_vec = chunk.to_vec();
173
+
let did_clone = did.clone();
174
+
175
+
self.db
176
+
.run(move |conn| {
177
+
use rsky_pds::schema::pds::repo_block::dsl::*;
178
+
179
+
diesel::delete(repo_block)
180
+
.filter(did.eq(&did_clone))
181
+
.filter(cid.eq_any(&chunk_vec))
182
+
.execute(conn)
183
+
})
184
+
.await?;
185
+
}
186
+
187
+
Ok(())
188
+
}
189
+
190
+
/// Get the detailed root information
191
+
pub async fn get_root_detailed(&self) -> Result<CidAndRev> {
192
+
let did = self.did.clone();
193
+
194
+
let root = self
195
+
.db
196
+
.run(move |conn| {
197
+
use rsky_pds::schema::pds::repo_root::dsl::*;
198
+
199
+
repo_root
200
+
.filter(did.eq(&did))
201
+
.first::<RepoRoot>(conn)
202
+
.optional()
203
+
})
204
+
.await?;
205
+
206
+
match root {
207
+
Some(r) => Ok(CidAndRev {
208
+
cid: Cid::from_str(&r.cid)?,
209
+
rev: r.rev,
210
+
}),
211
+
None => Err(anyhow::Error::new(RepoRootNotFoundError)),
212
+
}
213
+
}
214
+
}
215
+
216
+
impl ReadableBlockstore for SqlRepoStorage {
217
+
fn get_bytes<'a>(
218
+
&'a self,
219
+
cid: &'a Cid,
220
+
) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>>> + Send + Sync + 'a>> {
221
+
let did = self.did.clone();
222
+
let cid = cid.clone();
223
+
224
+
Box::pin(async move {
225
+
// Check cache first
226
+
{
227
+
let cache_guard = self.cache.read().await;
228
+
if let Some(cached) = cache_guard.get(cid) {
229
+
return Ok(Some(cached.clone()));
230
+
}
231
+
}
232
+
233
+
// Not in cache, query database
234
+
let cid_str = cid.to_string();
235
+
let result = self
236
+
.db
237
+
.run(move |conn| {
238
+
use rsky_pds::schema::pds::repo_block::dsl::*;
239
+
240
+
repo_block
241
+
.filter(did.eq(&did))
242
+
.filter(cid.eq(&cid_str))
243
+
.select(content)
244
+
.first::<Vec<u8>>(conn)
245
+
.optional()
246
+
})
247
+
.await?;
248
+
249
+
// Update cache if found
250
+
if let Some(content) = &result {
251
+
let mut cache_guard = self.cache.write().await;
252
+
cache_guard.set(cid, content.clone());
253
+
}
254
+
255
+
Ok(result)
256
+
})
257
+
}
258
+
259
+
fn has<'a>(
260
+
&'a self,
261
+
cid: Cid,
262
+
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + Sync + 'a>> {
263
+
Box::pin(async move {
264
+
let bytes = self.get_bytes(&cid).await?;
265
+
Ok(bytes.is_some())
266
+
})
267
+
}
268
+
269
+
fn get_blocks<'a>(
270
+
&'a self,
271
+
cids: Vec<Cid>,
272
+
) -> Pin<Box<dyn Future<Output = Result<BlocksAndMissing>> + Send + Sync + 'a>> {
273
+
Box::pin(async move {
274
+
// Check cache first
275
+
let cached = {
276
+
let mut cache_guard = self.cache.write().await;
277
+
cache_guard.get_many(cids.clone())?
278
+
};
279
+
280
+
if cached.missing.is_empty() {
281
+
return Ok(cached);
282
+
}
283
+
284
+
// Prepare data structures for missing blocks
285
+
let missing = CidSet::new(Some(cached.missing.clone()));
286
+
let missing_strings: Vec<String> =
287
+
cached.missing.iter().map(|c| c.to_string()).collect();
288
+
let did = self.did.clone();
289
+
290
+
// Create block map for results
291
+
let mut blocks = BlockMap::new();
292
+
let mut missing_set = CidSet::new(Some(cached.missing.clone()));
293
+
294
+
// Query database in chunks
295
+
for chunk in missing_strings.chunks(100) {
296
+
let chunk_vec = chunk.to_vec();
297
+
let did_clone = did.clone();
298
+
299
+
let rows = self
300
+
.db
301
+
.run(move |conn| {
302
+
use rsky_pds::schema::pds::repo_block::dsl::*;
303
+
304
+
repo_block
305
+
.filter(did.eq(&did_clone))
306
+
.filter(cid.eq_any(&chunk_vec))
307
+
.select((cid, content))
308
+
.load::<(String, Vec<u8>)>(conn)
309
+
})
310
+
.await?;
311
+
312
+
// Process results
313
+
for (cid_str, content) in rows {
314
+
let block_cid = Cid::from_str(&cid_str)?;
315
+
blocks.set(block_cid, content.clone());
316
+
missing_set.delete(block_cid);
317
+
318
+
// Update cache
319
+
let mut cache_guard = self.cache.write().await;
320
+
cache_guard.set(block_cid, content);
321
+
}
322
+
}
323
+
324
+
// Combine with cached blocks
325
+
blocks.add_map(cached.blocks)?;
326
+
327
+
Ok(BlocksAndMissing {
328
+
blocks,
329
+
missing: missing_set.to_list(),
330
+
})
331
+
})
332
+
}
333
+
}
334
+
335
+
impl RepoStorage for SqlRepoStorage {
336
+
fn get_root<'a>(&'a self) -> Pin<Box<dyn Future<Output = Option<Cid>> + Send + Sync + 'a>> {
337
+
Box::pin(async move {
338
+
match self.get_root_detailed().await {
339
+
Ok(root) => Some(root.cid),
340
+
Err(_) => None,
341
+
}
342
+
})
343
+
}
344
+
345
+
fn put_block<'a>(
346
+
&'a self,
347
+
cid: Cid,
348
+
bytes: Vec<u8>,
349
+
rev: String,
350
+
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + Sync + 'a>> {
351
+
let did = self.did.clone();
352
+
let bytes_clone = bytes.clone();
353
+
354
+
Box::pin(async move {
355
+
let cid_str = cid.to_string();
356
+
let size = bytes.len() as i32;
357
+
358
+
self.db
359
+
.run(move |conn| {
360
+
use rsky_pds::schema::pds::repo_block::dsl::*;
361
+
362
+
diesel::insert_into(repo_block)
363
+
.values((
364
+
did.eq(&did),
365
+
cid.eq(&cid_str),
366
+
repoRev.eq(&rev),
367
+
size.eq(size),
368
+
content.eq(&bytes),
369
+
))
370
+
.on_conflict_do_nothing()
371
+
.execute(conn)
372
+
})
373
+
.await?;
374
+
375
+
// Update cache
376
+
let mut cache_guard = self.cache.write().await;
377
+
cache_guard.set(cid, bytes_clone);
378
+
379
+
Ok(())
380
+
})
381
+
}
382
+
383
+
fn put_many<'a>(
384
+
&'a self,
385
+
to_put: BlockMap,
386
+
rev: String,
387
+
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + Sync + 'a>> {
388
+
let did = self.did.clone();
389
+
390
+
Box::pin(async move {
391
+
if to_put.size() == 0 {
392
+
return Ok(());
393
+
}
394
+
395
+
// Prepare blocks for insertion
396
+
let blocks: Vec<(String, String, String, i32, Vec<u8>)> = to_put
397
+
.map
398
+
.iter()
399
+
.map(|(cid, bytes)| {
400
+
(
401
+
did.clone(),
402
+
cid.to_string(),
403
+
rev.clone(),
404
+
bytes.0.len() as i32,
405
+
bytes.0.clone(),
406
+
)
407
+
})
408
+
.collect();
409
+
410
+
// Process in chunks
411
+
for chunk in blocks.chunks(50) {
412
+
let chunk_vec = chunk.to_vec();
413
+
414
+
self.db
415
+
.run(move |conn| {
416
+
use rsky_pds::schema::pds::repo_block::dsl::*;
417
+
418
+
let values: Vec<_> = chunk_vec
419
+
.iter()
420
+
.map(|(did_val, cid_val, rev_val, size_val, content_val)| {
421
+
(
422
+
did.eq(did_val),
423
+
cid.eq(cid_val),
424
+
repoRev.eq(rev_val),
425
+
size.eq(*size_val),
426
+
content.eq(content_val),
427
+
)
428
+
})
429
+
.collect();
430
+
431
+
diesel::insert_into(repo_block)
432
+
.values(&values)
433
+
.on_conflict_do_nothing()
434
+
.execute(conn)
435
+
})
436
+
.await?;
437
+
}
438
+
439
+
// Update cache with all blocks
440
+
{
441
+
let mut cache_guard = self.cache.write().await;
442
+
for (cid, bytes) in &to_put.map {
443
+
cache_guard.set(*cid, bytes.0.clone());
444
+
}
445
+
}
446
+
447
+
Ok(())
448
+
})
449
+
}
450
+
451
+
fn update_root<'a>(
452
+
&'a self,
453
+
cid: Cid,
454
+
rev: String,
455
+
is_create: Option<bool>,
456
+
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + Sync + 'a>> {
457
+
let did = self.did.clone();
458
+
let now = self.now.clone();
459
+
let is_create = is_create.unwrap_or(false);
460
+
461
+
Box::pin(async move {
462
+
let cid_str = cid.to_string();
463
+
464
+
if is_create {
465
+
// Insert new root
466
+
self.db
467
+
.run(move |conn| {
468
+
use rsky_pds::schema::pds::repo_root::dsl::*;
469
+
470
+
diesel::insert_into(repo_root)
471
+
.values((
472
+
did.eq(&did),
473
+
cid.eq(&cid_str),
474
+
rev.eq(&rev),
475
+
indexedAt.eq(&now),
476
+
))
477
+
.execute(conn)
478
+
})
479
+
.await?;
480
+
} else {
481
+
// Update existing root
482
+
self.db
483
+
.run(move |conn| {
484
+
use rsky_pds::schema::pds::repo_root::dsl::*;
485
+
486
+
diesel::update(repo_root)
487
+
.filter(did.eq(&did))
488
+
.set((cid.eq(&cid_str), rev.eq(&rev), indexedAt.eq(&now)))
489
+
.execute(conn)
490
+
})
491
+
.await?;
492
+
}
493
+
494
+
Ok(())
495
+
})
496
+
}
497
+
498
+
fn apply_commit<'a>(
499
+
&'a self,
500
+
commit: CommitData,
501
+
is_create: Option<bool>,
502
+
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + Sync + 'a>> {
503
+
Box::pin(async move {
504
+
// Apply commit in three steps
505
+
self.update_root(commit.cid, commit.rev.clone(), is_create)
506
+
.await?;
507
+
self.put_many(commit.new_blocks, commit.rev).await?;
508
+
self.delete_many(commit.removed_cids.to_list()).await?;
509
+
510
+
Ok(())
511
+
})
512
+
}
513
+
}
514
+
515
+
#[async_trait::async_trait]
516
+
impl AsyncBlockStoreRead for SqlRepoStorage {
517
+
async fn read_block(&mut self, cid: Cid) -> Result<Vec<u8>, BlockstoreError> {
518
+
let bytes = self
519
+
.get_bytes(&cid)
520
+
.await
521
+
.map_err(|e| BlockstoreError::Other(Box::new(e)))?
522
+
.ok_or(BlockstoreError::CidNotFound)?;
523
+
524
+
Ok(bytes)
525
+
}
526
+
527
+
fn read_block_into(
528
+
&mut self,
529
+
cid: Cid,
530
+
contents: &mut Vec<u8>,
531
+
) -> impl Future<Output = Result<(), BlockstoreError>> + Send {
532
+
async move {
533
+
let bytes = self.read_block(cid).await?;
534
+
contents.clear();
535
+
contents.extend_from_slice(&bytes);
536
+
Ok(())
537
+
}
538
+
}
539
+
}
540
+
541
+
#[async_trait::async_trait]
542
+
impl AsyncBlockStoreWrite for SqlRepoStorage {
543
+
fn write_block(
544
+
&mut self,
545
+
codec: u64,
546
+
hash: u64,
547
+
contents: &[u8],
548
+
) -> impl Future<Output = Result<Cid, BlockstoreError>> + Send {
549
+
let contents = contents.to_vec();
550
+
let rev = self.now.clone();
551
+
552
+
async move {
553
+
// Calculate digest based on hash algorithm
554
+
let digest = match hash {
555
+
atrium_repo::blockstore::SHA2_256 => sha2::Sha256::digest(&contents),
556
+
_ => return Err(BlockstoreError::UnsupportedHash(hash)),
557
+
};
558
+
559
+
// Create multihash
560
+
let multihash = atrium_repo::Multihash::wrap(hash, &digest)
561
+
.map_err(|_| BlockstoreError::UnsupportedHash(hash))?;
562
+
563
+
// Create CID
564
+
let cid = Cid::new_v1(codec, multihash);
565
+
566
+
// Store the block
567
+
self.put_block(cid, contents, rev)
568
+
.await
569
+
.map_err(|e| BlockstoreError::Other(Box::new(e)))?;
570
+
571
+
Ok(cid)
572
+
}
573
+
}
574
+
}
-94
src/db/cast.rs
-94
src/db/cast.rs
···
1
-
//! Type-safe casting utilities.
2
-
3
-
use serde::{Serialize, de::DeserializeOwned};
4
-
use std::fmt;
5
-
6
-
/// Represents an ISO 8601 date string (e.g., "2023-01-01T12:00:00Z").
7
-
#[derive(Debug, Clone, PartialEq, Eq)]
8
-
pub struct DateISO(String);
9
-
10
-
impl DateISO {
11
-
/// Converts a `chrono::DateTime<Utc>` to a `DateISO`.
12
-
pub fn from_date(date: chrono::DateTime<chrono::Utc>) -> Self {
13
-
Self(date.to_rfc3339())
14
-
}
15
-
16
-
/// Converts a `DateISO` back to a `chrono::DateTime<Utc>`.
17
-
pub fn to_date(&self) -> Result<chrono::DateTime<chrono::Utc>, chrono::ParseError> {
18
-
self.0.parse::<chrono::DateTime<chrono::Utc>>()
19
-
}
20
-
}
21
-
22
-
impl fmt::Display for DateISO {
23
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24
-
write!(f, "{}", self.0)
25
-
}
26
-
}
27
-
28
-
/// Represents a JSON-encoded string.
29
-
#[derive(Debug, Clone, PartialEq, Eq)]
30
-
pub struct JsonEncoded<T: Serialize>(String, std::marker::PhantomData<T>);
31
-
32
-
impl<T: Serialize> JsonEncoded<T> {
33
-
/// Encodes a value into a JSON string.
34
-
pub fn to_json(value: &T) -> Result<Self, serde_json::Error> {
35
-
let json = serde_json::to_string(value)?;
36
-
Ok(Self(json, std::marker::PhantomData))
37
-
}
38
-
39
-
/// Decodes a JSON string back into a value.
40
-
pub fn from_json(json_str: &str) -> Result<T, serde_json::Error>
41
-
where
42
-
T: DeserializeOwned,
43
-
{
44
-
serde_json::from_str(json_str)
45
-
}
46
-
47
-
/// Returns the underlying JSON string.
48
-
pub fn as_str(&self) -> &str {
49
-
&self.0
50
-
}
51
-
}
52
-
53
-
impl<T: Serialize> fmt::Display for JsonEncoded<T> {
54
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55
-
write!(f, "{}", self.0)
56
-
}
57
-
}
58
-
59
-
#[cfg(test)]
60
-
mod tests {
61
-
use super::*;
62
-
use chrono::Utc;
63
-
use serde::{Deserialize, Serialize};
64
-
65
-
#[test]
66
-
fn test_date_iso() {
67
-
let now = Utc::now();
68
-
let date_iso = DateISO::from_date(now);
69
-
let parsed_date = date_iso.to_date().unwrap();
70
-
assert_eq!(now.to_rfc3339(), parsed_date.to_rfc3339());
71
-
}
72
-
73
-
#[derive(Serialize, Deserialize, PartialEq, Debug)]
74
-
struct TestStruct {
75
-
name: String,
76
-
value: i32,
77
-
}
78
-
79
-
#[test]
80
-
fn test_json_encoded() {
81
-
let test_value = TestStruct {
82
-
name: "example".to_string(),
83
-
value: 42,
84
-
};
85
-
86
-
// Encode to JSON
87
-
let encoded = JsonEncoded::to_json(&test_value).unwrap();
88
-
assert_eq!(encoded.as_str(), r#"{"name":"example","value":42}"#);
89
-
90
-
// Decode from JSON
91
-
let decoded: TestStruct = JsonEncoded::from_json(encoded.as_str()).unwrap();
92
-
assert_eq!(decoded, test_value);
93
-
}
94
-
}
-112
src/db/db.rs
-112
src/db/db.rs
···
1
-
//! Database connection and transaction management.
2
-
3
-
use sqlx::{
4
-
Sqlite, Transaction,
5
-
sqlite::{SqliteConnectOptions, SqlitePool, SqliteQueryResult, SqliteTransactionManager},
6
-
};
7
-
use std::collections::VecDeque;
8
-
use std::str::FromStr;
9
-
use std::sync::{Arc, Mutex};
10
-
use tokio::sync::Mutex as AsyncMutex;
11
-
12
-
use crate::db::util::retry_sqlite;
13
-
14
-
/// Default pragmas for SQLite.
15
-
const DEFAULT_PRAGMAS: &[(&str, &str)] = &[
16
-
// Add default pragmas here if needed, e.g., ("foreign_keys", "ON")
17
-
];
18
-
19
-
/// Database struct for managing SQLite connections and transactions.
20
-
#[derive(Clone)]
21
-
pub(crate) struct Database {
22
-
/// SQLite connection pool.
23
-
pub(crate) pool: SqlitePool,
24
-
/// Flag indicating if the database is destroyed.
25
-
pub(crate) destroyed: Arc<Mutex<bool>>,
26
-
/// Queue of commit hooks.
27
-
pub(crate) commit_hooks: Arc<AsyncMutex<VecDeque<Box<dyn FnOnce() + Send>>>>,
28
-
}
29
-
30
-
impl Database {
31
-
/// Creates a new database instance with the given location and optional pragmas.
32
-
pub async fn new(location: &str, pragmas: Option<&[(&str, &str)]>) -> sqlx::Result<Self> {
33
-
let mut options = SqliteConnectOptions::from_str(location)?.create_if_missing(true);
34
-
35
-
// Apply default and user-provided pragmas.
36
-
for &(key, value) in DEFAULT_PRAGMAS.iter().chain(pragmas.unwrap_or(&[])) {
37
-
options = options.pragma(key.to_string(), value.to_string());
38
-
}
39
-
40
-
let pool = SqlitePool::connect_with(options).await?;
41
-
Ok(Self {
42
-
pool,
43
-
destroyed: Arc::new(Mutex::new(false)),
44
-
commit_hooks: Arc::new(AsyncMutex::new(VecDeque::new())),
45
-
})
46
-
}
47
-
48
-
/// Ensures the database is using Write-Ahead Logging (WAL) mode.
49
-
pub async fn ensure_wal(&self) -> sqlx::Result<()> {
50
-
let mut conn = self.pool.acquire().await?;
51
-
sqlx::query("PRAGMA journal_mode = WAL")
52
-
.execute(&mut *conn)
53
-
.await?;
54
-
Ok(())
55
-
}
56
-
57
-
/// Executes a transaction without retry logic.
58
-
pub async fn transaction_no_retry<F, T>(&self, func: F) -> sqlx::Result<T>
59
-
where
60
-
F: FnOnce(&mut Transaction<'_, Sqlite>) -> sqlx::Result<T>,
61
-
{
62
-
let mut tx = self.pool.begin().await?;
63
-
let result = func(&mut tx)?;
64
-
tx.commit().await?;
65
-
self.run_commit_hooks().await;
66
-
Ok(result)
67
-
}
68
-
69
-
/// Executes a transaction with retry logic.
70
-
pub async fn transaction<F, T>(&self, func: F) -> sqlx::Result<T>
71
-
where
72
-
F: FnOnce(&mut Transaction<'_, Sqlite>) -> sqlx::Result<T> + Copy,
73
-
{
74
-
retry_sqlite(|| self.transaction_no_retry(func)).await
75
-
}
76
-
77
-
/// Executes a query with retry logic.
78
-
pub async fn execute_with_retry<F, T>(&self, query: F) -> sqlx::Result<T>
79
-
where
80
-
F: Fn() -> std::pin::Pin<Box<dyn futures::Future<Output = sqlx::Result<T>> + Send>> + Copy,
81
-
{
82
-
retry_sqlite(|| query()).await
83
-
}
84
-
85
-
/// Adds a commit hook to be executed after a successful transaction.
86
-
pub async fn on_commit<F>(&self, hook: F)
87
-
where
88
-
F: FnOnce() + Send + 'static,
89
-
{
90
-
let mut hooks = self.commit_hooks.lock().await;
91
-
hooks.push_back(Box::new(hook));
92
-
}
93
-
94
-
/// Closes the database connection pool.
95
-
pub async fn close(&self) -> sqlx::Result<()> {
96
-
let mut destroyed = self.destroyed.lock().unwrap();
97
-
if *destroyed {
98
-
return Ok(());
99
-
}
100
-
*destroyed = true;
101
-
drop(self.pool.clone()); // Drop the pool to close connections.
102
-
Ok(())
103
-
}
104
-
105
-
/// Runs all commit hooks in the queue.
106
-
async fn run_commit_hooks(&self) {
107
-
let mut hooks = self.commit_hooks.lock().await;
108
-
while let Some(hook) = hooks.pop_front() {
109
-
hook();
110
-
}
111
-
}
112
-
}
-64
src/db/migrator.rs
-64
src/db/migrator.rs
···
1
-
//! Database migration management.
2
-
3
-
use sqlx::{SqlitePool, migrate::Migrator};
4
-
use std::path::Path;
5
-
use thiserror::Error;
6
-
7
-
/// Error type for migration-related issues.
8
-
#[derive(Debug, Error)]
9
-
pub enum MigrationError {
10
-
#[error("Migration failed: {0}")]
11
-
MigrationFailed(String),
12
-
#[error("Unknown failure occurred while migrating")]
13
-
UnknownFailure,
14
-
}
15
-
16
-
/// Migrator struct for managing database migrations.
17
-
pub struct DatabaseMigrator {
18
-
/// SQLx migrator instance.
19
-
migrator: Migrator,
20
-
/// SQLite connection pool.
21
-
db: SqlitePool,
22
-
}
23
-
24
-
impl DatabaseMigrator {
25
-
/// Creates a new `DatabaseMigrator` instance.
26
-
///
27
-
/// # Arguments
28
-
/// - `migrations_path`: Path to the directory containing migration files.
29
-
/// - `db`: SQLite connection pool.
30
-
pub async fn new(migrations_path: &Path, db: SqlitePool) -> Self {
31
-
let migrator = Migrator::new(migrations_path)
32
-
.await
33
-
.expect("Failed to initialize migrator");
34
-
Self { migrator, db }
35
-
}
36
-
37
-
/// Migrates the database to a specific migration or throws an error.
38
-
///
39
-
/// # Arguments
40
-
/// - `migration`: The target migration name.
41
-
///
42
-
/// # Unimplemented
43
-
/// This currently runs all migrations instead of a specific one.
44
-
pub async fn migrate_to_or_throw(&self, _migration: &str) -> Result<(), MigrationError> {
45
-
// TODO: Implement migration to a specific version
46
-
// For now, we will just run all migrations
47
-
let result = self.migrator.run(&self.db).await;
48
-
49
-
match result {
50
-
Ok(_) => Ok(()),
51
-
Err(err) => Err(MigrationError::MigrationFailed(err.to_string())),
52
-
}
53
-
}
54
-
55
-
/// Migrates the database to the latest migration or throws an error.
56
-
pub async fn migrate_to_latest_or_throw(&self) -> Result<(), MigrationError> {
57
-
let result = self.migrator.run(&self.db).await;
58
-
59
-
match result {
60
-
Ok(_) => Ok(()),
61
-
Err(err) => Err(MigrationError::MigrationFailed(err.to_string())),
62
-
}
63
-
}
64
-
}
+177
-7
src/db/mod.rs
+177
-7
src/db/mod.rs
···
1
-
mod cast;
2
-
mod db;
3
-
mod migrator;
4
-
mod pagination;
5
-
mod tables;
6
-
mod util;
1
+
use anyhow::{Context, Result};
2
+
use diesel::connection::SimpleConnection;
3
+
use diesel::prelude::*;
4
+
use diesel::r2d2::{self, ConnectionManager, Pool, PooledConnection};
5
+
use diesel::sqlite::{Sqlite, SqliteConnection};
6
+
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
7
+
use std::path::Path;
8
+
use std::sync::Arc;
9
+
use std::time::Duration;
10
+
11
+
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
12
+
pub type SqlitePool = Pool<ConnectionManager<SqliteConnection>>;
13
+
pub type SqlitePooledConnection = PooledConnection<ConnectionManager<SqliteConnection>>;
14
+
15
+
/// Database type for all queries
16
+
pub type DbType = Sqlite;
7
17
8
-
pub(crate) use db::Database;
18
+
/// Database connection wrapper
19
+
#[derive(Clone, Debug)]
20
+
pub struct DatabaseConnection {
21
+
pub pool: SqlitePool,
22
+
}
23
+
24
+
impl DatabaseConnection {
25
+
/// Create a new database connection with optional pragmas
26
+
pub async fn new(path: &str, pragmas: Option<&[(&str, &str)]>) -> Result<Self> {
27
+
// Create the database directory if it doesn't exist
28
+
if let Some(parent) = Path::new(path).parent() {
29
+
if !parent.exists() {
30
+
tokio::fs::create_dir_all(parent)
31
+
.await
32
+
.context(format!("Failed to create directory: {:?}", parent))?;
33
+
}
34
+
}
35
+
36
+
// Sanitize the path for connection string
37
+
let database_url = format!("sqlite:{}", path);
38
+
39
+
// Create a connection manager
40
+
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
41
+
42
+
// Create the connection pool with SQLite-specific configurations
43
+
let pool = r2d2::Pool::builder()
44
+
.max_size(10)
45
+
.connection_timeout(Duration::from_secs(30))
46
+
.test_on_check_out(true)
47
+
.build(manager)
48
+
.context("Failed to create connection pool")?;
49
+
50
+
// Initialize the database with pragmas
51
+
if let Some(pragmas) = pragmas {
52
+
let conn = &mut pool.get().context("Failed to get connection from pool")?;
53
+
54
+
// Apply all pragmas
55
+
for (pragma, value) in pragmas {
56
+
let sql = format!("PRAGMA {} = {}", pragma, value);
57
+
conn.batch_execute(&sql)
58
+
.context(format!("Failed to set pragma {}", pragma))?;
59
+
}
60
+
}
61
+
62
+
let db = DatabaseConnection { pool };
63
+
Ok(db)
64
+
}
65
+
66
+
/// Run migrations on the database
67
+
pub fn run_migrations(&self) -> Result<()> {
68
+
let mut conn = self
69
+
.pool
70
+
.get()
71
+
.context("Failed to get connection for migrations")?;
72
+
conn.run_pending_migrations(MIGRATIONS)
73
+
.context("Failed to run migrations")?;
74
+
Ok(())
75
+
}
76
+
77
+
/// Ensure WAL mode is enabled
78
+
pub async fn ensure_wal(&self) -> Result<()> {
79
+
let conn = &mut self.pool.get().context("Failed to get connection")?;
80
+
conn.batch_execute("PRAGMA journal_mode = WAL;")?;
81
+
conn.batch_execute("PRAGMA synchronous = NORMAL;")?;
82
+
conn.batch_execute("PRAGMA foreign_keys = ON;")?;
83
+
Ok(())
84
+
}
85
+
86
+
/// Execute a database operation with retries for busy errors
87
+
pub async fn run<F, T>(&self, operation: F) -> Result<T>
88
+
where
89
+
F: FnOnce(&mut SqliteConnection) -> QueryResult<T> + Send,
90
+
T: Send + 'static,
91
+
{
92
+
let mut retries = 0;
93
+
let max_retries = 5;
94
+
let mut last_error = None;
95
+
96
+
while retries < max_retries {
97
+
let mut conn = self.pool.get().context("Failed to get connection")?;
98
+
match operation(&mut conn) {
99
+
Ok(result) => return Ok(result),
100
+
Err(diesel::result::Error::DatabaseError(
101
+
diesel::result::DatabaseErrorKind::DatabaseIsLocked,
102
+
_,
103
+
)) => {
104
+
retries += 1;
105
+
let backoff_ms = 10 * (1 << retries); // Exponential backoff
106
+
last_error = Some(diesel::result::Error::DatabaseError(
107
+
diesel::result::DatabaseErrorKind::DatabaseIsLocked,
108
+
Box::new("Database is locked".to_string()),
109
+
));
110
+
tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
111
+
}
112
+
Err(e) => return Err(e.into()),
113
+
}
114
+
}
115
+
116
+
Err(anyhow::anyhow!(
117
+
"Max retries exceeded: {}",
118
+
last_error.unwrap_or(diesel::result::Error::RollbackTransaction(Box::new(
119
+
"Unknown error"
120
+
)))
121
+
))
122
+
}
123
+
124
+
/// Check if currently in a transaction
125
+
pub fn assert_transaction(&self) -> Result<()> {
126
+
// SQLite doesn't have a straightforward way to check transaction state
127
+
// We'll implement a simplified version that just returns Ok for now
128
+
Ok(())
129
+
}
130
+
131
+
/// Run a transaction with retry logic for busy database errors
132
+
pub async fn transaction<T, F>(&self, f: F) -> Result<T>
133
+
where
134
+
F: FnOnce(&mut SqliteConnection) -> Result<T> + Send,
135
+
T: Send + 'static,
136
+
{
137
+
self.run(|conn| {
138
+
conn.transaction(|tx| {
139
+
f(tx).map_err(|e| diesel::result::Error::RollbackTransaction(Box::new(e)))
140
+
})
141
+
})
142
+
.await
143
+
}
144
+
145
+
/// Run a transaction with no retry logic
146
+
pub async fn transaction_no_retry<T, F>(&self, f: F) -> Result<T>
147
+
where
148
+
F: FnOnce(&mut SqliteConnection) -> std::result::Result<T, diesel::result::Error> + Send,
149
+
T: Send + 'static,
150
+
{
151
+
let conn = &mut self
152
+
.pool
153
+
.get()
154
+
.context("Failed to get connection for transaction")?;
155
+
156
+
conn.transaction(f)
157
+
.map_err(|e| anyhow::anyhow!("Transaction error: {:?}", e))
158
+
}
159
+
}
160
+
161
+
/// Create a connection pool for SQLite
162
+
pub async fn create_sqlite_pool(database_url: &str) -> Result<SqlitePool> {
163
+
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
164
+
let pool = Pool::builder()
165
+
.max_size(10)
166
+
.connection_timeout(Duration::from_secs(30))
167
+
.test_on_check_out(true)
168
+
.build(manager)
169
+
.context("Failed to create connection pool")?;
170
+
171
+
// Apply recommended SQLite settings
172
+
let conn = &mut pool.get()?;
173
+
conn.batch_execute(
174
+
"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;",
175
+
)?;
176
+
177
+
Ok(pool)
178
+
}
-163
src/db/pagination.rs
-163
src/db/pagination.rs
···
1
-
use std::fmt::Debug;
2
-
3
-
/// Represents a cursor with primary and secondary parts.
4
-
#[derive(Debug, Clone)]
5
-
pub struct Cursor {
6
-
pub primary: String,
7
-
pub secondary: String,
8
-
}
9
-
10
-
/// Represents a labeled result with primary and secondary parts.
11
-
#[derive(Debug, Clone)]
12
-
pub struct LabeledResult {
13
-
pub primary: String,
14
-
pub secondary: String,
15
-
}
16
-
17
-
/// Trait defining the interface for a keyset-paginated cursor.
18
-
pub trait GenericKeyset<R, LR: Debug> {
19
-
fn label_result(&self, result: R) -> LR;
20
-
fn labeled_result_to_cursor(&self, labeled: LR) -> Cursor;
21
-
fn cursor_to_labeled_result(&self, cursor: Cursor) -> LR;
22
-
23
-
fn pack_from_result(&self, results: Vec<R>) -> Option<String> {
24
-
todo!()
25
-
// results
26
-
// .last()
27
-
// .map(|result| self.pack(Some(self.label_result(result.clone()))))
28
-
}
29
-
30
-
fn pack(&self, labeled: Option<LR>) -> Option<String> {
31
-
labeled.map(|l| self.pack_cursor(self.labeled_result_to_cursor(l)))
32
-
}
33
-
34
-
fn unpack(&self, cursor_str: Option<String>) -> Option<LR> {
35
-
cursor_str
36
-
.and_then(|cursor| self.unpack_cursor(cursor))
37
-
.map(|c| self.cursor_to_labeled_result(c))
38
-
}
39
-
40
-
fn pack_cursor(&self, cursor: Cursor) -> String {
41
-
format!("{}::{}", cursor.primary, cursor.secondary)
42
-
}
43
-
44
-
fn unpack_cursor(&self, cursor_str: String) -> Option<Cursor> {
45
-
let parts: Vec<&str> = cursor_str.split("::").collect();
46
-
if parts.len() == 2 {
47
-
Some(Cursor {
48
-
primary: parts[0].to_string(),
49
-
secondary: parts[1].to_string(),
50
-
})
51
-
} else {
52
-
None
53
-
}
54
-
}
55
-
}
56
-
57
-
/// A concrete implementation of `GenericKeyset` for time and CID-based pagination.
58
-
pub struct TimeCidKeyset;
59
-
60
-
impl TimeCidKeyset {
61
-
pub fn new() -> Self {
62
-
Self
63
-
}
64
-
}
65
-
66
-
impl GenericKeyset<CreatedAtCidResult, LabeledResult> for TimeCidKeyset {
67
-
fn label_result(&self, result: CreatedAtCidResult) -> LabeledResult {
68
-
LabeledResult {
69
-
primary: result.created_at,
70
-
secondary: result.cid,
71
-
}
72
-
}
73
-
74
-
fn labeled_result_to_cursor(&self, labeled: LabeledResult) -> Cursor {
75
-
Cursor {
76
-
primary: labeled.primary,
77
-
secondary: labeled.secondary,
78
-
}
79
-
}
80
-
81
-
fn cursor_to_labeled_result(&self, cursor: Cursor) -> LabeledResult {
82
-
LabeledResult {
83
-
primary: cursor.primary,
84
-
secondary: cursor.secondary,
85
-
}
86
-
}
87
-
}
88
-
89
-
/// Represents a database result with created_at and cid fields.
90
-
#[derive(Debug, Clone)]
91
-
pub struct CreatedAtCidResult {
92
-
pub created_at: String,
93
-
pub cid: String,
94
-
}
95
-
96
-
/// Pagination options for queries.
97
-
pub struct PaginationOptions<'a> {
98
-
pub limit: Option<usize>,
99
-
pub cursor: Option<String>,
100
-
pub direction: Option<&'a str>,
101
-
pub try_index: Option<bool>,
102
-
}
103
-
104
-
/// Applies pagination to a query.
105
-
pub fn paginate<K>(query: &mut String, opts: PaginationOptions, keyset: &K) -> String
106
-
where
107
-
K: GenericKeyset<CreatedAtCidResult, LabeledResult>,
108
-
{
109
-
let PaginationOptions {
110
-
limit,
111
-
cursor,
112
-
direction,
113
-
try_index,
114
-
} = opts;
115
-
116
-
let direction = direction.unwrap_or("desc");
117
-
let labeled = cursor.and_then(|c| keyset.unpack(Some(c)));
118
-
let keyset_sql = labeled.map(|l| get_sql(&l, direction, try_index.unwrap_or(false)));
119
-
120
-
if let Some(sql) = keyset_sql {
121
-
query.push_str(&format!(" WHERE {}", sql));
122
-
}
123
-
124
-
if let Some(l) = limit {
125
-
query.push_str(&format!(" LIMIT {}", l));
126
-
}
127
-
128
-
query.push_str(&format!(
129
-
" ORDER BY primary {} secondary {}",
130
-
direction, direction
131
-
));
132
-
133
-
query.clone()
134
-
}
135
-
136
-
/// Generates SQL conditions for pagination.
137
-
fn get_sql(labeled: &LabeledResult, direction: &str, try_index: bool) -> String {
138
-
if try_index {
139
-
if direction == "asc" {
140
-
format!(
141
-
"(primary, secondary) > ('{}', '{}')",
142
-
labeled.primary, labeled.secondary
143
-
)
144
-
} else {
145
-
format!(
146
-
"(primary, secondary) < ('{}', '{}')",
147
-
labeled.primary, labeled.secondary
148
-
)
149
-
}
150
-
} else {
151
-
if direction == "asc" {
152
-
format!(
153
-
"(primary > '{}' OR (primary = '{}' AND secondary > '{}'))",
154
-
labeled.primary, labeled.primary, labeled.secondary
155
-
)
156
-
} else {
157
-
format!(
158
-
"(primary < '{}' OR (primary = '{}' AND secondary < '{}'))",
159
-
labeled.primary, labeled.primary, labeled.secondary
160
-
)
161
-
}
162
-
}
163
-
}
-1
src/db/tables/mod.rs
-1
src/db/tables/mod.rs
···
1
-
mod moderation;
-72
src/db/tables/moderation.rs
-72
src/db/tables/moderation.rs
···
1
-
//! Moderation-related database table definitions.
2
-
3
-
use serde::{Deserialize, Serialize};
4
-
use sqlx::FromRow;
5
-
6
-
/// Table names for moderation-related entities.
7
-
pub const ACTION_TABLE_NAME: &str = "moderation_action";
8
-
pub const ACTION_SUBJECT_BLOB_TABLE_NAME: &str = "moderation_action_subject_blob";
9
-
pub const REPORT_TABLE_NAME: &str = "moderation_report";
10
-
pub const REPORT_RESOLUTION_TABLE_NAME: &str = "moderation_report_resolution";
11
-
12
-
/// Represents a moderation action.
13
-
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
14
-
#[sqlx(rename_all = "camelCase")]
15
-
pub struct ModerationAction {
16
-
pub id: i32, // Auto-generated ID
17
-
pub action: String,
18
-
pub subject_type: String,
19
-
pub subject_did: String,
20
-
pub subject_uri: Option<String>,
21
-
pub subject_cid: Option<String>,
22
-
pub create_label_vals: Option<String>,
23
-
pub negate_label_vals: Option<String>,
24
-
pub comment: Option<String>,
25
-
pub created_at: String,
26
-
pub created_by: String,
27
-
pub duration_in_hours: Option<i32>,
28
-
pub expires_at: Option<String>,
29
-
pub meta: Option<std::collections::HashMap<String, serde_json::Value>>,
30
-
}
31
-
32
-
/// Represents a subject blob associated with a moderation action.
33
-
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
34
-
#[sqlx(rename_all = "camelCase")]
35
-
pub struct ModerationActionSubjectBlob {
36
-
pub action_id: i32,
37
-
pub cid: String,
38
-
pub record_uri: String,
39
-
}
40
-
41
-
/// Represents a moderation report.
42
-
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
43
-
#[sqlx(rename_all = "camelCase")]
44
-
pub struct ModerationReport {
45
-
pub id: i32, // Auto-generated ID
46
-
pub subject_type: String,
47
-
pub subject_did: String,
48
-
pub subject_uri: Option<String>,
49
-
pub subject_cid: Option<String>,
50
-
pub reason_type: String,
51
-
pub reason: Option<String>,
52
-
pub reported_by_did: String,
53
-
pub created_at: String,
54
-
}
55
-
56
-
/// Represents a resolution for a moderation report.
57
-
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
58
-
#[sqlx(rename_all = "camelCase")]
59
-
pub struct ModerationReportResolution {
60
-
pub report_id: i32,
61
-
pub action_id: i32,
62
-
pub created_at: String,
63
-
pub created_by: String,
64
-
}
65
-
66
-
/// Represents a partial database schema for moderation-related tables.
67
-
pub struct PartialDB {
68
-
pub moderation_action: Vec<ModerationAction>,
69
-
pub moderation_action_subject_blob: Vec<ModerationActionSubjectBlob>,
70
-
pub moderation_report: Vec<ModerationReport>,
71
-
pub moderation_report_resolution: Vec<ModerationReportResolution>,
72
-
}
-124
src/db/util.rs
-124
src/db/util.rs
···
1
-
//! This module contains utility functions and types for working with SQLite databases using SQLx.
2
-
3
-
use sqlx::Error;
4
-
use std::collections::HashSet;
5
-
6
-
/// Returns a SQL clause to check if a record is not soft-deleted.
7
-
pub fn not_soft_deleted_clause(alias: &str) -> String {
8
-
format!(r#"{}."takedownRef" IS NULL"#, alias)
9
-
}
10
-
11
-
/// Checks if a record is soft-deleted.
12
-
pub fn is_soft_deleted(takedown_ref: Option<&str>) -> bool {
13
-
takedown_ref.is_some()
14
-
}
15
-
16
-
/// SQL clause to count all rows.
17
-
pub const COUNT_ALL: &str = "COUNT(*)";
18
-
19
-
/// SQL clause to count distinct rows based on a reference.
20
-
pub fn count_distinct(ref_col: &str) -> String {
21
-
format!("COUNT(DISTINCT {})", ref_col)
22
-
}
23
-
24
-
/// Generates a SQL clause for the `excluded` column in an `ON CONFLICT` clause.
25
-
pub fn excluded(col: &str) -> String {
26
-
format!("excluded.{}", col)
27
-
}
28
-
29
-
/// Generates a SQL clause for a large `WHERE IN` clause using a hash lookup.
30
-
/// # DEPRECATED
31
-
/// Use SQLx parameterized queries instead.
32
-
#[deprecated = "Use SQLx parameterized queries instead"]
33
-
pub fn values_list(vals: &[&str]) -> String {
34
-
let values = vals
35
-
.iter()
36
-
.map(|val| format!("('{}')", val))
37
-
.collect::<Vec<_>>()
38
-
.join(", ");
39
-
format!("(VALUES {})", values)
40
-
}
41
-
42
-
/// Retries an asynchronous SQLite operation with exponential backoff.
43
-
pub async fn retry_sqlite<F, Fut, T>(operation: F) -> Result<T, sqlx::Error>
44
-
where
45
-
F: Fn() -> Fut,
46
-
Fut: std::future::Future<Output = Result<T, sqlx::Error>>,
47
-
{
48
-
let max_retries = 60;
49
-
let mut attempt = 0;
50
-
51
-
while attempt < max_retries {
52
-
match operation().await {
53
-
Ok(result) => return Ok(result),
54
-
Err(err) if is_retryable_sqlite_error(&err) => {
55
-
if let Some(wait_ms) = get_wait_ms_sqlite(attempt, 5000) {
56
-
tokio::time::sleep(std::time::Duration::from_millis(wait_ms)).await;
57
-
attempt += 1;
58
-
} else {
59
-
return Err(err);
60
-
}
61
-
}
62
-
Err(err) => return Err(err),
63
-
}
64
-
}
65
-
66
-
Err(sqlx::Error::Protocol("Max retries exceeded".into()))
67
-
}
68
-
69
-
/// Checks if an error is retryable for SQLite.
70
-
fn is_retryable_sqlite_error(err: &Error) -> bool {
71
-
matches!(
72
-
err,
73
-
Error::Database(db_err) if {
74
-
let code = db_err.code().unwrap_or_default().to_string();
75
-
RETRY_ERRORS.contains(code.as_str())
76
-
}
77
-
)
78
-
}
79
-
80
-
/// Calculates the wait time for retries based on SQLite's backoff strategy.
81
-
fn get_wait_ms_sqlite(attempt: usize, timeout: u64) -> Option<u64> {
82
-
const DELAYS: [u64; 12] = [1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100];
83
-
const TOTALS: [u64; 12] = [0, 1, 3, 8, 18, 33, 53, 78, 103, 128, 178, 228];
84
-
85
-
if attempt >= DELAYS.len() {
86
-
let delay = DELAYS.last().unwrap();
87
-
let prior = TOTALS.last().unwrap() + delay * (attempt as u64 - (DELAYS.len() as u64 - 1));
88
-
if prior + delay > timeout {
89
-
return None;
90
-
}
91
-
Some(*delay)
92
-
} else {
93
-
let delay = DELAYS[attempt];
94
-
let prior = TOTALS[attempt];
95
-
if prior + delay > timeout {
96
-
None
97
-
} else {
98
-
Some(delay)
99
-
}
100
-
}
101
-
}
102
-
103
-
/// Checks if an error is a unique constraint violation.
104
-
pub fn is_err_unique_violation(err: &Error) -> bool {
105
-
matches!(
106
-
err,
107
-
Error::Database(db_err) if {
108
-
let code = db_err.code().unwrap_or_default();
109
-
code == "23505" || code == "SQLITE_CONSTRAINT_UNIQUE"
110
-
}
111
-
)
112
-
}
113
-
114
-
lazy_static::lazy_static! {
115
-
/// Set of retryable SQLite error codes.
116
-
static ref RETRY_ERRORS: HashSet<&'static str> = {
117
-
let mut set = HashSet::new();
118
-
set.insert("SQLITE_BUSY");
119
-
set.insert("SQLITE_BUSY_SNAPSHOT");
120
-
set.insert("SQLITE_BUSY_RECOVERY");
121
-
set.insert("SQLITE_BUSY_TIMEOUT");
122
-
set
123
-
};
124
-
}