Alternative ATProto PDS implementation

prototype actor_store; begin convert from sqlx to diesel; reorganize

+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 11 11 mod prepared_write; 12 12 mod record; 13 13 mod repo; 14 + mod sql_repo; 14 15 15 16 pub(crate) use actor_store::ActorStore; 16 17 pub(crate) use actor_store_reader::ActorStoreReader;
+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
··· 1 - //! Preference handling for actor store. 2 - 3 - mod reader; 4 - mod transactor; 5 - 6 - pub(crate) use reader::PreferenceReader; 7 - pub(crate) use transactor::PreferenceTransactor;
-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
··· 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
··· 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
··· 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
··· 1 - //! Record storage and retrieval for the actor store. 2 - 3 - mod reader; 4 - mod transactor; 5 - 6 - pub(crate) use reader::RecordReader; 7 - pub(crate) use transactor::RecordTransactor;
-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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 - mod moderation;
-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
··· 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 - }